¿Cómo usar GROUP BY para concatenar cadenas en SQL Server?
-
07-07-2019 - |
Pregunta
Cómo obtengo:
id Name Value
1 A 4
1 B 8
2 C 9
a
id Column
1 A:4, B:8
2 C:9
Solución
No se necesita CURSOR, WHILE loop o 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
Otros consejos
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
el uso de la ruta XML no se concatenará perfectamente como cabría esperar ... reemplazará " & amp; " con " & amp; amp; " y también se meterá con <" and ">
... tal vez algunas otras cosas, no estoy seguro ... pero puedes probar esto
Encontré una solución para esto ... necesita 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 tiene SQL
una función agregada concatenada? esto es una PITA.
Me encontré con un par de problemas cuando intenté convertir la sugerencia de Kevin Fairchild para trabajar con cadenas que contienen espacios y caracteres XML especiales (&
, <
, >
) que estaban 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. > < 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 soluciona automáticamente mediante TYPE directiva.
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
Instale los agregados SQLCLR de http://groupconcat.codeplex.com
Luego puede escribir 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;
SQL Server 2005 y versiones posteriores le permiten crear sus propias funciones agregadas personalizadas , incluso para cosas como la concatenación, vea la muestra al final del artículo vinculado.
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 Vista previa técnica de la comunidad 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
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
Resultado en
name type
------------
name1 type1
name2 type2; type3
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.
Solo para agregar a lo que dijo Cade, esto suele ser 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 exportación de archivos u otros & Quot; SQL only & Quot; soluciones, pero la mayoría de las veces esta concatenación debe manejarse en su capa de visualización.
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
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
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 su consulta.
no vio 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
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