Как использовать GROUP BY для объединения строк в SQL Server?

373

Как я могу получить:

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

в

id          Column
1          A:4, B:8
2          C:9
Eldila
источник
18
Этот тип проблемы легко решается на MySQL с помощью его GROUP_CONCAT()агрегатной функции, но решить ее на Microsoft SQL Server гораздо сложнее. Для получения справки см. Следующий вопрос: « Как получить несколько записей для одной записи на основе отношения? »
Билл Карвин,
1
Каждый, у кого есть учетная запись Microsoft, должен проголосовать за более простое решение для подключения: connect.microsoft.com/SQLServer/feedback/details/427987/…
Йенс Мюленхофф,
1
Вы можете использовать SQLCLR Aggregates, найденные здесь, в качестве замены, пока T-SQL не будет расширен: groupconcat.codeplex.com
Orlando Colamatteo
1
Дубликат stackoverflow.com/questions/194852/…
Салман A

Ответы:

550

Не требуется CURSOR, цикл WHILE или пользовательская функция .

Просто нужно быть креативным с FOR XML и PATH.

[Примечание: это решение работает только на SQL 2005 и более поздних версиях. В оригинальном вопросе не указана используемая версия.]

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
Кевин Фэйрчайлд
источник
6
почему бы не поставить временную таблицу?
Эми Б
3
Это самая крутая вещь SQL, которую я видел в своей жизни. Любая идея, если это «быстро» для больших наборов данных? Он не начинает ползти, как курсор или что-то еще, не так ли? Я хотел бы, чтобы больше людей проголосовали за это безумие.
user12861
6
Эх. Я просто ненавижу стиль этого подзапроса. СОЕДИНЕНИЯ намного приятнее. Только не думайте, что я смогу использовать это в этом решении. В любом случае, я рад видеть, что здесь помимо меня есть другие придурки SQL, которым нравится изучать подобные вещи. Слава всем вам :)
Кевин Фэйрчайлд
6
Немного более чистый способ работы со строками: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')), 1,2, '') AS NameValues
Джонатан
3
Просто чтобы отметить кое-что, что я нашел. Даже в нечувствительной к регистру среде часть .value запроса ДОЛЖНА быть строчной. Я предполагаю, что это потому, что это XML, который чувствителен к регистру
Jaloopa
137

Если это SQL Server 2017 или SQL Server Vnext, SQL Azure вы можете использовать string_agg, как показано ниже:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id
Каннан Кандасами
источник
Работает безупречно!
Argoo
1
Это прекрасно работает, лучше, чем принятый ответ.
Янник Бреунис
51

использование пути XML не будет идеально объединено, как вы могли бы ожидать ... оно заменит "&" на "& amp;" и также будет связываться с <" and "> ... может быть, несколько других вещей, не уверен ... но вы можете попробовать это

Я нашел обходной путь для этого ... вам нужно заменить:

FOR XML PATH('')
)

с:

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

... или NVARCHAR(MAX)если это то, что вы используете.

почему, черт возьми, нет SQLобъединенной агрегатной функции? это пита

Аллен
источник
2
Я искал сеть в поисках лучшего способа НЕ кодировать вывод. Спасибо вам большое! Это окончательный ответ - пока MS не добавит соответствующую поддержку, как агрегатная функция CONCAT (). Что я делаю, это добавляю это в Outer-Apply, которое возвращает мое объединенное поле. Я не фанат добавления nested-select в мои операторы select.
MikeTeeVee
Я согласился, что без использования Value мы можем столкнуться с проблемами, когда текст является символом XML. Пожалуйста, найдите в моем блоге сценарии групповой конкатенации на SQL-сервере. blog.vcillusion.co.in/...
vCillusion
40

Я столкнулся с несколькими проблемами , когда я попытался преобразовать предложение Kevin Fairchild для работы со строками , содержащих пробелы и специальные символы XML ( &, <, >) , которые кодируются.

Окончательная версия моего кода (которая не отвечает на первоначальный вопрос, но может быть полезна кому-то) выглядит следующим образом:

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

Вместо использования пробела в качестве разделителя и замены всех пробелов запятыми, он просто добавляет запятую и пробел к каждому значению, а затем использует их STUFFдля удаления первых двух символов.

Кодировка XML обеспечивается автоматически с помощью директивы TYPE .

Джонатан Сэйс
источник
21

Еще один вариант использования Sql Server 2005 и выше

---- 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
источник
Спасибо за вклад, я всегда предпочитаю использовать CTE и рекурсивные CTE для решения проблем на сервере SQL. Это работает один работает для меня здорово!
gbdavid
Можно ли использовать его в запросе с внешним применить?
огонь в дыре
14

Установите агрегаты SQLCLR с http://groupconcat.codeplex.com

Затем вы можете написать такой код, чтобы получить результат, который вы просили:

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;
Орландо Коламаттео
источник
Я использовал его несколько лет назад, синтаксис намного чище, чем все уловки «XML Path», и работает очень хорошо. Я настоятельно рекомендую это, когда функции SQL CLR являются опцией.
ПОСЛЕ 13.09.16
12

SQL Server 2005 и более поздние версии позволяют создавать собственные настраиваемые агрегатные функции , в том числе для таких вещей, как конкатенация - см. Образец внизу связанной статьи.

Джоэл Коухорн
источник
4
К сожалению, это требует (?) Использования сборок CLR .. что является еще одной
1
Просто пример использует CLR для фактической реализации конкатенации, но это не обязательно. Вы можете сделать так, чтобы агрегатная функция конкатенации использовала FOR XML, так что, по крайней мере, ее лучше вызывать в будущем!
Шив
12

Восемь лет спустя ... Ядро базы данных Microsoft SQL Server vNext наконец-то улучшило Transact-SQL для прямой поддержки конкатенации сгруппированных строк. В техническом обзоре сообщества версии 1.0 добавлена ​​функция STRING_AGG, а в CTP 1.1 добавлено предложение WITHIN GROUP для функции STRING_AGG.

Ссылка: https://msdn.microsoft.com/en-us/library/mt775028.aspx

Шем Сарджент
источник
9

Это всего лишь дополнение к посту Кевина Фэйрчайлда (кстати, очень умный). Я бы добавил это как комментарий, но у меня пока недостаточно очков :)

Я использовал эту идею для представления, над которым работал, однако элементы, которые я объединял, содержали пробелы. Поэтому я немного изменил код, чтобы не использовать пробелы в качестве разделителей.

Еще раз спасибо за классный обходной путь, Кевин!

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
источник
9

Примером будет

В Oracle вы можете использовать агрегатную функцию LISTAGG.

Оригинальные записи

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

SQL

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

Результат в

name   type
------------
name1  type1
name2  type2; type3
Михал Б.
источник
6
Выглядит хорошо, но вопросы конкретно не об Oracle.
user12861
13
Я понимаю. Но я искал то же самое для Оракула, поэтому я решил поставить его здесь для других людей, таких как я :)
Михал Б.
@MichalB. Вы не пропустили внутри синтаксис? Например: listagg (тип, ',') внутри группы (порядок по имени)?
Gregory
@gregory: я отредактировал свой ответ. Я думаю, что мое старое решение работало в те времена. Текущая форма, которую вы предложили, будет работать точно, спасибо.
Михал Б.
1
для будущих людей - вы можете написать новый вопрос с собственным ответом для существенной разницы, например, для другой платформы
Майк М
7

Этот тип вопросов здесь задают очень часто, и решение будет во многом зависеть от базовых требований:

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

а также

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

Как правило, нет единственного SQL-способа сделать это без динамического sql, пользовательской функции или курсора.

Кейд Ру
источник
2
Не правда. Решение cyberkiwi с использованием cte: s - это чистый sql без каких-либо специфических для производителя взломов.
Бьорн Линдквист
1
Во время вопросов и ответов я бы не считал рекурсивные CTE ужасно переносимыми, но теперь они поддерживаются Oracle. Лучшее решение будет зависеть от платформы. Для SQL Server это, скорее всего, метод FOR XML или агрегат CLR клиента.
Каде Ру
1
окончательный ответ на все вопросы? stackoverflow.com/search?q=[wh независимо от вопроса]
Junchen Liu
7

Просто чтобы добавить к тому, что сказал Кейд, это, как правило, интерфейсный дисплей, и поэтому его следует обрабатывать там. Я знаю, что иногда проще написать что-то на 100% в SQL для таких вещей, как экспорт файлов или другие решения «только для SQL», но в большинстве случаев эта конкатенация должна обрабатываться на вашем уровне отображения.

Том Х
источник
11
Группировка - это сейчас внешний интерфейс? Существует множество допустимых сценариев объединения одного столбца в сгруппированный набор результатов.
MGOwen
5

Не нужен курсор ... цикла while достаточно.

------------------------------
-- 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, возможно, лучшая критика в том, что PRIMARY KEY должен быть объявлен в переменных таблицы.
Эми Б
@marc_s При дальнейшем осмотре эта статья является фиктивной, как и почти все обсуждения производительности без измерения IO. Я узнал о LAG - так что спасибо за это.
Эми Б
4

Давайте получим очень просто:

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

Заменить эту строку:

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

С вашим запросом.

Маркиньо Пели
источник
3

не видел перекрестных ответов, также не нужно извлекать XML. Вот немного другая версия того, что написал Кевин Фэйрчайлд. Это быстрее и проще для использования в более сложных запросах:

   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
Без использования Value мы можем столкнуться с проблемами, когда текст является символом в кодировке XML
vCillusion
2

Вы можете значительно повысить производительность следующим образом, если группа по содержит в основном один элемент:

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
Эдуард
источник
Предполагая, что вы не хотите дублировать имена в списке, что вы можете или не можете.
jnm2
1

Использование функции замены и 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

Для образца данных и других способов нажмите здесь

Махеш
источник
1

Если у вас включен clr, вы можете использовать библиотеку Group_Concat из GitHub

Манфред Виппель
источник