Comment utiliser GROUP BY pour concaténer des chaînes dans SQL Server?

373

Comment puis-je obtenir:

id       Name       Value
1          A          4
1          B          8
2          C          9

à

id          Column
1          A:4, B:8
2          C:9
Eldila
la source
18
Ce type de problème est résolu facilement sur MySQL avec sa GROUP_CONCAT()fonction d'agrégation, mais le résoudre sur Microsoft SQL Server est plus délicat. Voir la question SO suivante pour obtenir de l'aide: " Comment obtenir plusieurs enregistrements contre un enregistrement basé sur la relation? "
Bill Karwin
1
Tout le monde avec un compte Microsoft devrait voter pour une solution plus simple sur connect: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff
1
Vous pouvez utiliser les agrégats SQLCLR trouvés ici comme substitut jusqu'à ce que T-SQL soit amélioré: groupconcat.codeplex.com
Orlando Colamatteo
1
Duplicate de stackoverflow.com/questions/194852/…
Salman A

Réponses:

550

Aucun curseur, boucle WHILE ou fonction définie par l'utilisateur n'est nécessaire .

Il suffit d'être créatif avec FOR XML et PATH.

[Remarque: Cette solution ne fonctionne que sur SQL 2005 et versions ultérieures. La question d'origine ne spécifiait pas la version utilisée.]

CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT 
  [ID],
  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID

DROP TABLE #YourTable
Kevin Fairchild
la source
6
pourquoi voudrait-on nolock une table temporaire?
Amy B
3
C'est la chose SQL la plus cool que j'ai vue de ma vie. Une idée si c'est "rapide" pour les grands ensembles de données? Il ne commence pas à ramper comme le ferait un curseur ou quoi que ce soit, non? Je souhaite que plus de gens votent pour cette folie.
user12861
6
Eh. Je déteste juste le style de sous-requête de celui-ci. Les JOINS sont tellement plus agréables. Ne pensez pas que je peux utiliser cela dans cette solution. Quoi qu'il en soit, je suis heureux de voir qu'il y a d'autres dorks SQL ici en dehors de moi qui aiment apprendre des trucs comme ça. Bravo à vous tous :)
Kevin Fairchild
6
Une façon légèrement plus propre de faire la manipulation de chaîne: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')), 1,2, '') AS NameValues
Jonathan Sayce
3
Juste pour noter quelque chose que j'ai trouvé. Même dans un environnement insensible à la casse, la partie .value de la requête doit être en minuscules. Je suppose que c'est parce que c'est XML, qui est sensible à la casse
Jaloopa
137

S'il s'agit de SQL Server 2017 ou SQL Server Vnext, SQL Azure, vous pouvez utiliser string_agg comme ci-dessous:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id
Kannan Kandasamy
la source
Fonctionne parfaitement!
argoo
1
Cela fonctionne très bien, mieux que la réponse acceptée.
Jannick Breunis
51

l'utilisation du chemin XML ne concaténera pas parfaitement comme vous pouvez vous y attendre ... il remplacera "&" par "& amp;" et va également jouer avec <" and "> ... peut-être quelques autres choses, je ne sais pas ... mais vous pouvez essayer cela

Je suis tombé sur une solution de contournement pour cela ... vous devez remplacer:

FOR XML PATH('')
)

avec:

FOR XML PATH(''),TYPE
).value('(./text())[1]','VARCHAR(MAX)')

... ou NVARCHAR(MAX)si c'est ce que vous utilisez.

pourquoi l'enfer n'a pas de SQLfonction d'agrégation concaténée? c'est un PITA.

Allen
la source
2
J'ai parcouru le net à la recherche de la meilleure façon de NE PAS encoder la sortie. Merci beaucoup! C'est la réponse définitive - jusqu'à ce que MS ajoute un support approprié pour cela, comme une fonction d'agrégation CONCAT (). Ce que je fais, c'est jeter ceci dans un Outer-Apply qui retourne mon champ concaténé. Je ne suis pas fan de l'ajout de sélections imbriquées dans mes instructions de sélection.
MikeTeeVee
J'ai accepté, sans utiliser Value, nous pouvons rencontrer des problèmes lorsque le texte est un caractère encodé XML. Veuillez trouver mon blog couvrant les scénarios de concaténation groupée dans SQL Server. blog.vcillusion.co.in/…
vCillusion
40

Je suis tombé sur quelques problèmes quand j'essayé de convertir la suggestion de Kevin Fairchild à travailler avec des chaînes contenant des espaces et des caractères spéciaux (XML &, <, >) qui ont été encodées.

La version finale de mon code (qui ne répond pas à la question d'origine mais peut être utile à quelqu'un) ressemble à ceci:

CREATE TABLE #YourTable ([ID] INT, [Name] VARCHAR(MAX), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'Oranges & Lemons',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'1 < 2',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT  [ID],
  STUFF((
    SELECT ', ' + CAST([Name] AS VARCHAR(MAX))
    FROM #YourTable WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE 
     /* Use .value to uncomment XML entities e.g. &gt; &lt; etc*/
    ).value('.','VARCHAR(MAX)') 
  ,1,2,'') as NameValues
FROM    #YourTable Results
GROUP BY ID

DROP TABLE #YourTable

Plutôt que d'utiliser un espace comme délimiteur et de remplacer tous les espaces par des virgules, il ajoute simplement une virgule et un espace à chaque valeur, puis les utilise STUFFpour supprimer les deux premiers caractères.

Le codage XML est pris en charge automatiquement à l'aide de la directive TYPE .

Jonathan Sayce
la source
21

Une autre option utilisant Sql Server 2005 et supérieur

---- test data
declare @t table (OUTPUTID int, SCHME varchar(10), DESCR varchar(10))
insert @t select 1125439       ,'CKT','Approved'
insert @t select 1125439       ,'RENO','Approved'
insert @t select 1134691       ,'CKT','Approved'
insert @t select 1134691       ,'RENO','Approved'
insert @t select 1134691       ,'pn','Approved'

---- actual query
;with cte(outputid,combined,rn)
as
(
  select outputid, SCHME + ' ('+DESCR+')', rn=ROW_NUMBER() over (PARTITION by outputid order by schme, descr)
  from @t
)
,cte2(outputid,finalstatus,rn)
as
(
select OUTPUTID, convert(varchar(max),combined), 1 from cte where rn=1
union all
select cte2.outputid, convert(varchar(max),cte2.finalstatus+', '+cte.combined), cte2.rn+1
from cte2
inner join cte on cte.OUTPUTID = cte2.outputid and cte.rn=cte2.rn+1
)
select outputid, MAX(finalstatus) from cte2 group by outputid
cyberkiwi
la source
Merci pour l'entrée, je préfère toujours utiliser les CTE et les CTE récursifs pour résoudre les problèmes dans le serveur SQL. C'est travaillé on travaille pour moi super!
gbdavid
est-il possible de l'utiliser dans une requête avec application externe?
feu dans le trou
14

Installez les agrégats SQLCLR à partir de http://groupconcat.codeplex.com

Ensuite, vous pouvez écrire du code comme celui-ci pour obtenir le résultat que vous avez demandé:

CREATE TABLE foo
(
 id INT,
 name CHAR(1),
 Value CHAR(1)
);

INSERT  INTO dbo.foo
    (id, name, Value)
VALUES  (1, 'A', '4'),
        (1, 'B', '8'),
        (2, 'C', '9');

SELECT  id,
    dbo.GROUP_CONCAT(name + ':' + Value) AS [Column]
FROM    dbo.foo
GROUP BY id;
Orlando Colamatteo
la source
Je l'ai utilisé il y a quelques années, la syntaxe est beaucoup plus propre que toutes les astuces "XML Path" et elle fonctionne très bien. Je le recommande fortement lorsque les fonctions SQL CLR sont une option.
AFract
12

SQL Server 2005 et versions ultérieures vous permettent de créer vos propres fonctions d'agrégation personnalisées , y compris pour des choses comme la concaténation - voir l'exemple au bas de l'article lié.

Joel Coehoorn
la source
4
Malheureusement, cela nécessite (?) L'utilisation d'assemblys CLR .. ce qui est un autre problème à résoudre: - /
1
Juste l'exemple utilise CLR pour l'implémentation de concaténation réelle, mais ce n'est pas obligatoire. Vous pourriez faire en sorte que la fonction d'agrégation de concaténation utilise FOR XML, donc au moins c'est plus pratique de l'appeler à l'avenir!
Shiv
12

Huit ans plus tard ... Le moteur de base de données Microsoft SQL Server vNext a enfin amélioré Transact-SQL pour prendre directement en charge la concaténation de chaînes groupées. La version 1.0 de Community Technical Preview a ajouté la fonction STRING_AGG et CTP 1.1 a ajouté la clause WITHIN GROUP pour la fonction STRING_AGG.

Référence: https://msdn.microsoft.com/en-us/library/mt775028.aspx

Shem Sargent
la source
9

Ceci n'est qu'un ajout au post de Kevin Fairchild (très intelligent d'ailleurs). Je l'aurais ajouté en commentaire, mais je n'ai pas encore assez de points :)

J'utilisais cette idée pour une vue sur laquelle je travaillais, mais les éléments que je concaténais contenaient des espaces. J'ai donc légèrement modifié le code pour ne pas utiliser d'espaces comme délimiteurs.

Encore merci pour la solution de contournement cool Kevin!

CREATE TABLE #YourTable ( [ID] INT, [Name] CHAR(1), [Value] INT ) 

INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'A', 4) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'B', 8) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (2, 'C', 9) 

SELECT [ID], 
       REPLACE(REPLACE(REPLACE(
                          (SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) as A 
                           FROM   #YourTable 
                           WHERE  ( ID = Results.ID ) 
                           FOR XML PATH (''))
                        , '</A><A>', ', ')
                ,'<A>','')
        ,'</A>','') AS NameValues 
FROM   #YourTable Results 
GROUP  BY ID 

DROP TABLE #YourTable 
Phillip
la source
9

Un exemple serait

Dans Oracle, vous pouvez utiliser la fonction d'agrégation LISTAGG.

Disques originaux

name   type
------------
name1  type1
name2  type2
name2  type3

Sql

SELECT name, LISTAGG(type, '; ') WITHIN GROUP(ORDER BY name)
FROM table
GROUP BY name

Aboutir à

name   type
------------
name1  type1
name2  type2; type3
Michal B.
la source
6
Ça a l'air bien, mais les questions ne concernent spécifiquement pas Oracle.
user12861
13
Je comprends. Mais je cherchais la même chose pour Oracle, alors j'ai pensé que je le mettrais ici pour d'autres personnes comme moi :)
Michal B.
@MichalB. Ne manquez-vous pas la syntaxe intra? par exemple: listagg (type, ',') au sein du groupe (ordre par nom)?
Gregory
@gregory: j'ai modifié ma réponse. Je pense que mon ancienne solution fonctionnait à l'époque. Le formulaire actuel que vous avez suggéré fonctionnera à coup sûr, merci.
Michal
1
pour les futurs gens - vous pouvez écrire une nouvelle question avec votre propre réponse pour une différence significative comme une plate
Mike M
7

Ce type de question est très souvent posé ici, et la solution va dépendre beaucoup des exigences sous-jacentes:

https://stackoverflow.com/search?q=sql+pivot

et

https://stackoverflow.com/search?q=sql+concatenate

En règle générale, il n'existe aucun moyen SQL uniquement de le faire sans SQL dynamique, une fonction définie par l'utilisateur ou un curseur.

Cade Roux
la source
2
Pas vrai. La solution de cyberkiwi utilisant cte: s est du sql pur sans aucun piratage spécifique au fournisseur.
Björn Lindqvist
1
Au moment de la question et de la réponse, je n'aurais pas considéré les CTE récursifs comme terriblement portables, mais ils sont désormais pris en charge par Oracle. La meilleure solution va dépendre de la plateforme. Pour SQL Server, il s'agit très probablement de la technique FOR XML ou d'un agrégat CLR client.
Cade Roux
1
la réponse ultime à toutes les questions? stackoverflow.com/search?q== quelle que soit la question]
Junchen Liu
7

Juste pour ajouter à ce que Cade a dit, il s'agit généralement d'un affichage frontal et devrait donc y être géré. Je sais qu'il est parfois plus facile d'écrire quelque chose à 100% en SQL pour des choses comme l'exportation de fichiers ou d'autres solutions "SQL uniquement", mais la plupart du temps cette concaténation doit être gérée dans votre couche d'affichage.

Tom H
la source
11
Le regroupement est une chose d'affichage frontal maintenant? Il existe de nombreux scénarios valides pour concaténer une colonne dans un ensemble de résultats groupés.
MGOwen
5

Pas besoin de curseur ... une boucle while est suffisante.

------------------------------
-- Setup
------------------------------

DECLARE @Source TABLE
(
  id int,
  Name varchar(30),
  Value int
)

DECLARE @Target TABLE
(
  id int,
  Result varchar(max) 
)


INSERT INTO @Source(id, Name, Value) SELECT 1, 'A', 4
INSERT INTO @Source(id, Name, Value) SELECT 1, 'B', 8
INSERT INTO @Source(id, Name, Value) SELECT 2, 'C', 9


------------------------------
-- Technique
------------------------------

INSERT INTO @Target (id)
SELECT id
FROM @Source
GROUP BY id

DECLARE @id int, @Result varchar(max)
SET @id = (SELECT MIN(id) FROM @Target)

WHILE @id is not null
BEGIN
  SET @Result = null

  SELECT @Result =
    CASE
      WHEN @Result is null
      THEN ''
      ELSE @Result + ', '
    END + s.Name + ':' + convert(varchar(30),s.Value)
  FROM @Source s
  WHERE id = @id

  UPDATE @Target
  SET Result = @Result
  WHERE id = @id

  SET @id = (SELECT MIN(id) FROM @Target WHERE @id < id)
END

SELECT *
FROM @Target
Amy B
la source
@marc_s peut-être une meilleure critique est que PRIMARY KEY devrait être déclarée sur les variables de la table.
Amy B
@marc_s Après une inspection plus approfondie, cet article est une imposture - comme le sont presque toutes les discussions sur les performances sans mesure d'E / S. J'ai entendu parler de LAG - merci pour cela.
Amy B
4

Soyons très simples:

SELECT stuff(
    (
    select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb 
    FOR XML PATH('')
    )
, 1, 2, '')

Remplacez cette ligne:

select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb

Avec votre requête.

Marquinho Peli
la source
3

n'a pas vu de réponses d'application croisée, également pas besoin d'extraction xml. Voici une version légèrement différente de ce que Kevin Fairchild a écrit. C'est plus rapide et plus facile à utiliser dans des requêtes plus complexes:

   select T.ID
,MAX(X.cl) NameValues
 from #YourTable T
 CROSS APPLY 
 (select STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
    FROM #YourTable 
    WHERE (ID = T.ID) 
    FOR XML PATH(''))
  ,1,2,'')  [cl]) X
  GROUP BY T.ID
Mordechai
la source
1
Sans utiliser Value, nous pouvons rencontrer des problèmes lorsque le texte est un caractère encodé XML
vCillusion
2

Vous pouvez améliorer les performances de manière significative si le regroupement par contient principalement un élément:

SELECT 
  [ID],

CASE WHEN MAX( [Name]) = MIN( [Name]) THEN 
MAX( [Name]) NameValues
ELSE

  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues

END

FROM #YourTable Results
GROUP BY ID
Eduard
la source
En supposant que vous ne voulez pas de noms en double dans la liste, que vous le souhaitiez ou non.
jnm2
1

Utilisation de la fonction Remplacer et POUR LE CHEMIN JSON

SELECT T3.DEPT, REPLACE(REPLACE(T3.ENAME,'{"ENAME":"',''),'"}','') AS ENAME_LIST
FROM (
 SELECT DEPT, (SELECT ENAME AS [ENAME]
        FROM EMPLOYEE T2
        WHERE T2.DEPT=T1.DEPT
        FOR JSON PATH,WITHOUT_ARRAY_WRAPPER) ENAME
    FROM EMPLOYEE T1
    GROUP BY DEPT) T3

Pour des exemples de données et d'autres moyens, cliquez ici

Mahesh
la source
1

Si vous avez activé clr, vous pouvez utiliser la bibliothèque Group_Concat de GitHub

Manfred Wippel
la source