Je trouve un moyen d'agréger des chaînes de différentes lignes en une seule ligne. Je cherche à faire cela dans de nombreux endroits, alors avoir une fonction pour faciliter cela serait Nice. J'ai essayé des solutions en utilisant COALESCE
et FOR XML
, mais ils ne me la coupent pas.
L'agrégation de chaînes ferait quelque chose comme ceci:
id | Name Result: id | Names
-- - ---- -- - -----
1 | Matt 1 | Matt, Rocks
1 | Rocks 2 | Stylus
2 | Stylus
J'ai jeté un oeil à fonctions d'agrégat définies par CLR en remplacement de COALESCE
et FOR XML
, mais apparemment SQL Azure ne le fait pas prend en charge les éléments définis par le CLR, ce qui est pénible pour moi car savoir que je pourrais l'utiliser résoudrait beaucoup de problèmes pour moi.
Existe-t-il une solution de contournement possible ou une méthode tout aussi optimale (qui pourrait ne pas être aussi optimale que CLR, mais hé je prendrai ce que je peux obtenir) que je peux utiliser pour agréger mes données?
SOLUTION
La définition de optimal peut varier, mais voici comment concaténer des chaînes de différentes lignes à l'aide de Transact SQL standard, ce qui devrait fonctionner correctement dans Azure.
;WITH Partitioned AS
(
SELECT
ID,
Name,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
COUNT(*) OVER (PARTITION BY ID) AS NameCount
FROM dbo.SourceTable
),
Concatenated AS
(
SELECT
ID,
CAST(Name AS nvarchar) AS FullName,
Name,
NameNumber,
NameCount
FROM Partitioned
WHERE NameNumber = 1
UNION ALL
SELECT
P.ID,
CAST(C.FullName + ', ' + P.Name AS nvarchar),
P.Name,
P.NameNumber,
P.NameCount
FROM Partitioned AS P
INNER JOIN Concatenated AS C
ON P.ID = C.ID
AND P.NameNumber = C.NameNumber + 1
)
SELECT
ID,
FullName
FROM Concatenated
WHERE NameNumber = NameCount
EXPLICATION
L'approche se résume à trois étapes:
Numérotez les lignes en utilisant OVER
et PARTITION
en les regroupant et en les classant selon les besoins pour la concaténation. Le résultat est Partitioned
CTE. Nous conservons le nombre de lignes dans chaque partition pour filtrer les résultats ultérieurement.
À l'aide de CTE récursif (Concatenated
), parcourez les numéros de ligne (NameNumber
colonne) en ajoutant Name
valeurs à FullName
colonne.
Filtrez tous les résultats sauf ceux avec le plus haut NameNumber
.
N'oubliez pas que pour rendre cette requête prévisible, vous devez définir à la fois le regroupement (par exemple, dans votre scénario, les lignes avec le même ID
sont concaténées) et le tri (j'ai supposé que vous triiez simplement la chaîne par ordre alphabétique). avant la concaténation).
J'ai rapidement testé la solution sur SQL Server 2012 avec les données suivantes:
INSERT dbo.SourceTable (ID, Name)
VALUES
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')
Le résultat de la requête:
ID FullName
----------- ------------------------------
2 Stylus
3 Bar, Baz, Foo
1 Matt, Rocks
Les méthodes utilisant FOR XML PATH, comme ci-dessous, sont-elles vraiment si lentes? Itzik Ben-Gan écrit que cette méthode présente de bonnes performances dans son livre d'interrogation T-SQL (M. Ben-Gan est une source digne de confiance, à mon avis).
create table #t (id int, name varchar(20))
insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')
select id
,Names = stuff((select ', ' + name as [text()]
from #t xt
where xt.id = t.id
for xml path('')), 1, 2, '')
from #t t
group by id
Pour ceux d'entre nous qui ont trouvé cela et n'utilisez pas la base de données SQL Azure:
STRING_AGG()
dans PostgreSQL, SQL Server 2017 et Azure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.Microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql
GROUP_CONCAT()
dans MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat
(Merci à @Brianjorden et @milanio pour la mise à jour Azure)
select Id
, STRING_AGG(Name, ', ') Names
from Demo
group by Id
Violon SQL: http://sqlfiddle.com/#!18/89251/1
Bien que @ réponse soit correcte, j'ai comparé la consommation de temps de son chemin avec xmlpath et j'ai trouvé que xmlpath était tellement plus rapide. Je vais écrire le code de comparaison et vous pouvez le vérifier vous-même. Ceci est la façon @serge:
DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;
set nocount on;
declare @YourTable table (ID int, Name nvarchar(50))
WHILE @counter < 1000
BEGIN
insert into @YourTable VALUES (ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
SET @counter = @counter + 1;
END
SET @startTime = GETDATE()
;WITH Partitioned AS
(
SELECT
ID,
Name,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
COUNT(*) OVER (PARTITION BY ID) AS NameCount
FROM @YourTable
),
Concatenated AS
(
SELECT ID, CAST(Name AS nvarchar) AS FullName, Name, NameNumber, NameCount FROM Partitioned WHERE NameNumber = 1
UNION ALL
SELECT
P.ID, CAST(C.FullName + ', ' + P.Name AS nvarchar), P.Name, P.NameNumber, P.NameCount
FROM Partitioned AS P
INNER JOIN Concatenated AS C ON P.ID = C.ID AND P.NameNumber = C.NameNumber + 1
)
SELECT
ID,
FullName
FROM Concatenated
WHERE NameNumber = NameCount
SET @endTime = GETDATE();
SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 54 milliseconds
Et c'est comme ça xmlpath:
DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;
set nocount on;
declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))
WHILE @counter < 1000
BEGIN
insert into @YourTable VALUES (@counter, ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
SET @counter = @counter + 1;
END
SET @startTime = GETDATE();
set nocount off
SELECT
t1.HeaderValue
,STUFF(
(SELECT
', ' + t2.ChildValue
FROM @YourTable t2
WHERE t1.HeaderValue=t2.HeaderValue
ORDER BY t2.ChildValue
FOR XML PATH(''), TYPE
).value('.','varchar(max)')
,1,2, ''
) AS ChildValues
FROM @YourTable t1
GROUP BY t1.HeaderValue
SET @endTime = GETDATE();
SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 4 milliseconds
Mise à jour: Ms SQL Server 2017+, base de données Azure SQL
Vous pouvez utiliser: STRING_AGG
.
L'utilisation est assez simple pour la demande d'OP:
SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id
Eh bien, mon ancienne non-réponse a été supprimée à juste titre (à gauche ci-dessous), mais s'il arrive que quelqu'un atterrisse ici à l'avenir, il y a de bonnes nouvelles. Ils ont également implémenté STRING_AGG () dans la base de données Azure SQL. Cela devrait fournir la fonctionnalité exacte demandée à l'origine dans cet article avec un support natif et intégré. @hrobky l'a mentionné précédemment en tant que fonctionnalité SQL Server 2016 à l'époque.
--- Old Post: Pas assez de réputation ici pour répondre à @hrobky directement, mais STRING_AGG a fière allure, mais il n'est disponible que dans SQL Server 2016 vNext pour le moment. Espérons que cela suivra bientôt dans Azure SQL Datababse.
Vous pouvez utiliser + = pour concaténer des chaînes, par exemple:
declare @test nvarchar(max)
set @test = ''
select @test += name from names
si vous sélectionnez @test, tous les noms seront concaténés
J'ai trouvé que la réponse de Serge était très prometteuse, mais j'ai également rencontré des problèmes de performances avec son écriture. Cependant, lorsque je l'ai restructuré pour utiliser des tables temporaires et ne pas inclure de tables à double CTE, la performance est passée de 1 minute 40 secondes à une sous-seconde pour 1 000 enregistrements combinés. Pour ceux qui en ont besoin sans FOR XML sur les anciennes versions de SQL Server:
DECLARE @STRUCTURED_VALUES TABLE (
ID INT
,VALUE VARCHAR(MAX) NULL
,VALUENUMBER BIGINT
,VALUECOUNT INT
);
INSERT INTO @STRUCTURED_VALUES
SELECT ID
,VALUE
,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY VALUE) AS VALUENUMBER
,COUNT(*) OVER (PARTITION BY ID) AS VALUECOUNT
FROM RAW_VALUES_TABLE;
WITH CTE AS (
SELECT SV.ID
,SV.VALUE
,SV.VALUENUMBER
,SV.VALUECOUNT
FROM @STRUCTURED_VALUES SV
WHERE VALUENUMBER = 1
UNION ALL
SELECT SV.ID
,CTE.VALUE + ' ' + SV.VALUE AS VALUE
,SV.VALUENUMBER
,SV.VALUECOUNT
FROM @STRUCTURED_VALUES SV
JOIN CTE
ON SV.ID = CTE.ID
AND SV.VALUENUMBER = CTE.VALUENUMBER + 1
)
SELECT ID
,VALUE
FROM CTE
WHERE VALUENUMBER = VALUECOUNT
ORDER BY ID
;