¿Cómo usar GROUP BY para concatenar cadenas en SQL Server?


373

Como lo consigo:

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

a

id          Column
1          A:4, B:8
2          C:9

18
Este tipo de problema se resuelve fácilmente en MySQL con su GROUP_CONCAT()función agregada, pero resolverlo en Microsoft SQL Server es más incómodo. Consulte la siguiente pregunta de SO para obtener ayuda: " ¿Cómo obtener registros múltiples contra un registro basado en la relación? "
Bill Karwin,

1
Todos los que tengan una cuenta de Microsoft deberían votar por una solución más simple en connect: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff el

1
Puede utilizar los agregados SQLCLR que se encuentran aquí como sustituto hasta que se mejore
Orlando Colamatteo

Respuestas:


550

No se necesita CURSOR, bucle WHILE ni función definida por el usuario .

Solo necesito ser creativo con FOR XML y PATH.

[Nota: esta solución solo funciona en SQL 2005 y versiones posteriores. La pregunta original no especificaba la versión en uso.]

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

66
¿Por qué uno podría bloquear una mesa temporal?
Amy B

3
Esta es la cosa SQL más genial que he visto en mi vida. ¿Alguna idea de si es "rápido" para grandes conjuntos de datos? No comienza a gatear como lo haría un cursor ni nada, ¿verdad? Desearía que más personas votaran por esta locura.
user12861

66
Eh Simplemente odio el estilo de subconsulta. Las uniones son mucho más agradables. Simplemente no creo que pueda utilizar eso en esta solución. De todos modos, me alegra ver que hay otros idiotas de SQL aquí aparte de mí que les gusta aprender cosas como esta. Felicitaciones a todos :)
Kevin Fairchild

66
Una forma un poco más limpia de hacer la manipulación de cadenas: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')), 1,2 '') AS NameValues
Jonathan Sayce

3
Solo para notar algo que he encontrado. Incluso en un entorno que no distingue entre mayúsculas y minúsculas, la parte .value de la consulta NECESITA ser minúscula. Supongo que esto se debe a que es XML, que
distingue entre

136

Si es SQL Server 2017 o SQL Server Vnext, SQL Azure puede usar string_agg de la siguiente manera:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id

Funciona impecable!
argoo

1
Esto funciona muy bien, mejor que la respuesta aceptada.
Jannick Breunis

51

el uso de la ruta XML no se concatenará perfectamente como podría esperarse ... reemplazará "&" con "& amp;" y también se meterá con <" and "> ... quizás algunas otras cosas, no estoy seguro ... pero puedes probar esto

Encontré una solución para esto ... debes reemplazar:

FOR XML PATH('')
)

con:

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

...o NVARCHAR(MAX) si eso es lo que estás usando.

por qué demonios no SQL tiene una función agregada concatenada? Esta es una PITA.


2
He recorrido la red buscando la mejor manera de NO codificar la salida. ¡Muchas gracias! Esta es la respuesta definitiva, hasta que MS agregue el soporte adecuado para esto, como una función agregada CONCAT (). Lo que hago es arrojar esto en una aplicación externa que devuelve mi campo concatenado. No soy fanático de agregar selecciones anidadas en mis declaraciones select.
MikeTeeVee

Estuve de acuerdo, sin usar Value, podemos encontrar problemas donde el texto es un carácter codificado en XML. Encuentre mi blog que cubre escenarios para la concatenación agrupada en el servidor SQL. blog.vcillusion.co.in/…
vCillusion

40

Me encontré con un par de problemas cuando he intentado convertir la sugerencia de Kevin Fairchild a trabajar con cadenas que contengan espacios y caracteres especiales (XML &, <, >), que fueron codificados.

La versión final de mi código (que no responde la pregunta original pero puede ser útil para alguien) se ve así:

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

En lugar de usar un espacio como delimitador y reemplazar todos los espacios con comas, simplemente antepone una coma y un espacio a cada valor y luego usa STUFF para eliminar los dos primeros caracteres.

La codificación XML se realiza automáticamente mediante la directiva TYPE .


21

Otra opción con Sql Server 2005 y superior

---- 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

Gracias por la entrada, siempre prefiero usar CTE y CTE recursivos para resolver problemas en el servidor SQL. ¡Esto funciona, uno me funciona muy bien!
gbdavid

¿es posible usarlo en una consulta con aplicación externa?
fuego en el hoyo

14

Instale los agregados SQLCLR desde http://groupconcat.codeplex.com

Luego puede escribir un código como este para obtener el resultado que solicitó:

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;

Lo utilicé hace unos años, la sintaxis es mucho más limpia que todos los trucos de "Ruta XML" y funciona muy bien. Lo recomiendo cuando las funciones SQL CLR son una opción.
AFract

12

SQL Server 2005 y versiones posteriores le permiten crear sus propias funciones agregadas personalizadas , incluso para cosas como la concatenación; consulte el ejemplo al final del artículo vinculado.


44
Desafortunadamente, esto requiere (?) El uso de ensamblajes CLR ... lo cual es otro problema para tratar: - /

1
Solo el ejemplo usa CLR para la implementación de concatenación real, pero esto no es obligatorio. ¡Podría hacer que la función de agregación de concatenación use FOR XML para que al menos sea mejor llamarlo en el futuro!
Shiv

12

Ocho años después ... Microsoft SQL Server vNext Database Engine finalmente ha mejorado Transact-SQL para admitir directamente la concatenación de cadenas agrupadas. La versión 1.0 de Community Technical Preview agregó la función STRING_AGG y CTP 1.1 agregó la cláusula WITHIN GROUP para la función STRING_AGG.

Referencia: https://msdn.microsoft.com/en-us/library/mt775028.aspx


9

Esto es solo una adición a la publicación de Kevin Fairchild (muy inteligente por cierto). Lo habría agregado como comentario, pero todavía no tengo suficientes puntos :)

Estaba usando esta idea para una vista en la que estaba trabajando, sin embargo, los elementos que estaba concatenando contenían espacios. Así que modifiqué el código ligeramente para no usar espacios como delimitadores.

Nuevamente, gracias por la genial solución 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 

9

Un ejemplo sería

En Oracle puede usar la función agregada LISTAGG.

Registros originales

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

SQL

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

Resulta en

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

66
Parece agradable, pero las preguntas no se refieren específicamente a Oracle.
user12861

13
Entiendo. Pero estaba buscando lo mismo para Oracle, así que pensé que lo pondría aquí para otras personas como yo :)
Michal B.

@MichalB. ¿No te estás perdiendo la sintaxis interna? por ejemplo: listagg (tipo, ',') dentro del grupo (ordenar por nombre)?
gregory

@gregory: edité mi respuesta. Creo que mi antigua solución solía funcionar en el pasado. El formulario actual que sugirió funcionará seguro, gracias.
Michal B.

1
para personas futuras: puede escribir una nueva pregunta con su propia respuesta para una diferencia significativa como una plataforma diferente
Mike M

7

Este tipo de pregunta se hace aquí muy a menudo, y la solución dependerá mucho de los requisitos subyacentes:

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

y

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

Por lo general, no existe una forma de hacer esto solo con SQL sin SQL dinámico, una función definida por el usuario o un cursor.


2
No es verdad. La solución de cyberkiwi que usa cte: s es sql puro sin ningún hacker específico del proveedor.
Björn Lindqvist

1
En el momento de la pregunta y la respuesta, no habría contado los CTE recursivos como terriblemente portátiles, pero ahora son compatibles con Oracle. La mejor solución dependerá de la plataforma. Para SQL Server es muy probable que sea la técnica FOR XML o un agregado CLR del cliente.
Cade Roux

1
¿La respuesta definitiva para todas las preguntas? stackoverflow.com/search?q=[cualquiera que sea la pregunta]
Junchen Liu

7

Solo para agregar a lo que dijo Cade, esto generalmente es algo de visualización frontal y, por lo tanto, debe manejarse allí. Sé que a veces es más fácil escribir algo 100% en SQL para cosas como la exportación de archivos u otras soluciones "solo SQL", pero la mayoría de las veces esta concatenación debe manejarse en su capa de visualización.


11
¿La agrupación es una cosa de visualización frontal ahora? Hay muchos escenarios válidos para concatenar una columna en un conjunto de resultados agrupados.
MGOwen

5

No necesito un cursor ... un bucle while es suficiente.

------------------------------
-- 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


@marc_s quizás una mejor crítica es que PRIMARY KEY debe declararse en las variables de la tabla.
Amy B

@marc_s En una inspección posterior, ese artículo es una farsa, como lo son casi todas las discusiones sobre el rendimiento sin medición de E / S. Aprendí sobre LAG, así que gracias por eso.
Amy B

4

Seamos muy simples:

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

Reemplace esta línea:

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

Con tu consulta.


3

no vi ninguna respuesta de aplicación cruzada, tampoco es necesario extraer xml. Aquí hay una versión ligeramente diferente de lo que escribió Kevin Fairchild. Es más rápido y fácil de usar en consultas más complejas:

   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

1
Sin usar Value, podemos encontrar problemas donde el texto es un carácter codificado en XML
vCillusion,

2

Puede mejorar el rendimiento de la siguiente manera si group by contiene principalmente un elemento:

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

Suponiendo que no desea nombres duplicados en la lista, lo que podría o no.
jnm2

1

Uso de la función Reemplazar y FOR JSON PATH

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

Para ver datos de muestra y más formas, haga clic aquí


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.