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

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

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

к

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

Этот тип проблемы легко решается в MySQL с его агрегатной функцией GROUP_CONCAT(), но решить ее на Microsoft SQL Server сложнее. См. Следующий вопрос SO для справки: "Как получить несколько записей против одной записи на основе отношения?"

Bill Karwin 07.11.2008 22:21

Каждый, у кого есть учетная запись Microsoft, должен проголосовать за более простое решение для подключения: connect.microsoft.com/SQLServer/feedback/details/427987/…

Jens Mühlenhoff 31.07.2015 13:45

Вы можете использовать агрегаты SQLCLR, найденные здесь, в качестве замены, пока T-SQL не будет улучшен: groupconcat.codeplex.com

Orlando Colamatteo 19.03.2016 06:29

Дубликат stackoverflow.com/questions/194852/…

Salman A 04.02.2018 21:23
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
404
4
601 616
21
Перейти к ответу Данный вопрос помечен как решенный

Ответы 21

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

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

и

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

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

Не правда. Решение cyberkiwi, использующее cte: s, представляет собой чистый sql без каких-либо хакерских действий, специфичных для производителя.

Björn Lindqvist 25.07.2013 15:24

Во время вопроса и ответа я бы не считал рекурсивные CTE ужасно переносимыми, но теперь они поддерживаются Oracle. Лучшее решение будет зависеть от платформы. Для SQL Server это, скорее всего, метод FOR XML или агрегат CLR клиента.

Cade Roux 25.07.2013 19:11

окончательный ответ на все вопросы? stackoverflow.com/search?q=[без того вопрос]

Junchen Liu 08.12.2016 14:07

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

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

MGOwen 03.08.2016 06:21

Не нужен курсор ... достаточно цикла 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 должен быть объявлен в переменных таблицы.

Amy B 10.03.2015 05:19

@marc_s При дальнейшем рассмотрении эта статья является фикцией, как и почти все обсуждения производительности без измерения ввода-вывода. Я действительно узнал о LAG - так что спасибо за это.

Amy B 10.03.2015 07:34
Ответ принят как подходящий

Не требуется CURSOR, WHILE loop или User-Defined Function..

Просто нужно проявить творческий подход к 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

почему нельзя заблокировать временную таблицу?

Amy B 07.11.2008 22:33

Это самая крутая вещь SQL, которую я видел в своей жизни. Есть идеи, «быстро» ли это для больших наборов данных? Он не начинает ползать, как курсор или что-то в этом роде, не так ли? Я хочу, чтобы больше людей проголосовали за это безумие.

user12861 08.11.2008 00:27

@mbrierst, этот пример создавался не с расчетом на эффективность. В основном просто хотел показать альтернативное решение. Когда вы спрашиваете, насколько быстро это работает для больших наборов данных, как вы определяете «большой» и «быстрый»? Я уверен, что есть более элегантный способ переписать его с большей эффективностью.

Kevin Fairchild 08.11.2008 00:40

Очень круто. В плане запроса указано, что выполняется только два сканирования таблицы и затем вложенный цикл. udf, вероятно, нельзя оптимизировать аналогичным образом. Обычно FOR XML не используют часто, мне нужно больше узнать о его хитростях.

Cade Roux 08.11.2008 00:49

Я совсем не уверен, что есть способ написать его более эффективно, если кто-нибудь знает его, пожалуйста, сделайте шаг вперед. И это не менее элегантно, чем отвратительные сводные операторы, добавленные в язык. Мне это так понравилось, что я нашел некоторые из ваших ответов и проголосовал за те, которые мне понравились.

user12861 08.11.2008 00:58

Эх. Я просто ненавижу его стиль подзапросов. JOINS намного приятнее. Только не думайте, что я смогу использовать это в этом решении. В любом случае, я рад видеть, что здесь есть и другие SQL-придурки, кроме меня, которым нравится изучать подобные вещи. Престижность всем вам :)

Kevin Fairchild 08.11.2008 01:02

Я тоже ненавижу подзапросы. Хорошая точка зрения. Но безумие FOR XML PATH закрыло мне глаза на все остальное в вашем запросе. Во всяком случае, до конца моей жизни. И я бы хотел, чтобы хоть один SQL-придурок, кроме меня, хоть раз поработал в моей компании.

user12861 08.11.2008 01:10

Немного более чистый способ обработки строк: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML ПУТЬ ('')), 1,2, '') AS NameValues

Jonathan Sayce 18.10.2011 14:54

Замечательно и безумно :-) Я использовал немного другую версию для получения всех таблиц в базе данных: SELECT STUFF ((SELECT ',' + [Name] FROM sys.objects WHERE [type] = 'U' FOR XML PATH ('' )), 1, 2, '') AS [Таблицы] FROM sys.objects WHERE [type] = 'U' GROUP BY [type]

Shabi_669 12.07.2013 01:55

Просто чтобы отметить то, что я нашел. Даже в среде, нечувствительной к регистру, часть запроса .value НЕОБХОДИМО быть в нижнем регистре. Я предполагаю, что это потому, что это XML, который чувствителен к регистру

Jaloopa 30.07.2013 19:22

Мне неловко для SQLServer, что это единственное решение этой проблемы без использования переменных.

Jim Pedid 27.02.2014 03:09

Помните: включите в подзапрос WHERE все столбцы в GROUP BY, которые необходимы для одинаковой степени уникальности. Например, если у вас GROUP BY ID, ORDERNO, то вам может понадобиться WHERE (ID = Results.ID) AND (ORDERNO = Results.ORDERNO)

Doug_Ivison 22.08.2014 23:17

На самом деле это скорее обходной путь, чем ответ. Что делать, если у вас очень большой запрос без столбца простого идентификатора и много + объединений + подзапросов + групп по условиям в представлении? Скопируйте и вставьте весь запрос в предложение «FOR XML» (для каждого столбца, к которому вы хотите присоединиться), действительно ли это лучшее решение, которое может предложить SQL Server? Я думаю, что реальный ответ заключается в том, что до тех пор, пока конкатенация строк 2017 г. не была изначально поддержана SQL Server. Очень огорчающе :(

David Rogers 21.04.2017 01:22

@DavidRogers, возможно, ваши запросы нужно еще упростить, прежде чем использовать эту технику. В любом случае, я был рад видеть, что STRING_AGG WITHIN GROUP был добавлен в SQL 2017, и настоятельно рекомендую его людям, использующим эту версию или позже. Поскольку эта функция не была доступна в то время, когда был дан ответ на этот вопрос, а SQL 2017 все еще находится в предварительной версии и не используется большинством пользователей, пытающихся найти решение этой проблемы на SQL Server до 2017 года, я поддерживаю это как «ответ», а не «обходной путь». :)

Kevin Fairchild 21.04.2017 16:24

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

К сожалению, это требует (?) Использования сборок CLR .. это еще одна проблема, с которой нужно иметь дело: - /

user166390 12.07.2012 00:27

Только в этом примере CLR используется для фактической реализации конкатенации, но это не требуется. Вы можете заставить агрегатную функцию конкатенации использовать FOR XML, так что, по крайней мере, будет удобнее вызывать ее в будущем!

Shiv 26.09.2016 07:45

Другой вариант с использованием 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

Спасибо за ввод, я всегда предпочитаю использовать CTE и рекурсивные CTE для решения проблем на сервере SQL. Это сработало, у меня отлично работает!

gbdavid 29.05.2015 11:57

можно ли использовать его в запросе с внешним применением?

fire in the hole 28.10.2015 13:01

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

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

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

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 

Я столкнулся с парой проблем, когда попытался преобразовать предложение Кевина Фэйрчайлда для работы со строками, содержащими пробелы и специальные символы 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 выполняется автоматически с помощью директивы ТИП.

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

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

FOR XML PATH('')
)

с:

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

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

какого черта у SQL нет агрегатной функции конкатенации? это ПИТА.

Я обыскал сеть в поисках лучшего способа НЕ кодировать вывод. Большое спасибо! Это окончательный ответ - до тех пор, пока MS не добавит для этого надлежащую поддержку, например агрегатную функцию CONCAT (). Что я делаю, так это бросаю это во внешнее приложение, которое возвращает мое объединенное поле. Я не поклонник добавления вложенных выборок в мои операторы выбора.

MikeTeeVee 15.03.2013 23:01

Я согласился, что без использования Value мы можем столкнуться с проблемами, когда текст представляет собой символ в кодировке XML. В моем блоге описаны сценарии групповой конкатенации на сервере SQL. blog.vcillusion.co.in/…

vCillusion 03.06.2018 01:23

Примером может быть

В 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

Выглядит неплохо, но вопросы конкретно не об Oracle.

user12861 07.02.2013 19:43

Я понимаю. Но я искал то же самое для Oracle, поэтому я подумал, что выложу это здесь для других людей, таких как я :)

Michal B. 08.02.2013 14:36

@MichalB. Разве вы не упускаете из виду внутренний синтаксис? например: listagg (type, ',') внутри группы (по имени)?

gregory 11.02.2017 00:52

@gregory: Я отредактировал свой ответ. Я думаю, что в свое время мое старое решение работало. Текущая форма, которую вы предложили, наверняка будет работать, спасибо.

Michal B. 15.02.2017 13:57

для будущих людей - вы можете написать новый вопрос со своим собственным ответом для значительного различия, например, разные платформы

Mike M 22.03.2018 14:15

Получим очень просто:

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

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

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

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 30.06.2016 17:35

Установите агрегаты 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.

AFract 13.09.2016 11:24

Восемь лет спустя ... Microsoft SQL Server vNext Database Engine наконец-то усовершенствовал Transact-SQL для прямой поддержки конкатенации сгруппированных строк. Версия 1.0 Community Technical Preview добавила функцию STRING_AGG, а CTP 1.1 добавил предложение WITHIN GROUP для функции STRING_AGG.

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

не видел никаких перекрестных ответов, также нет необходимости в извлечении 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

Без использования Value мы можем столкнуться с проблемами, когда текст представляет собой символ в кодировке XML.

vCillusion 03.06.2018 01:20

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

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

Работает безупречно!

argoo 03.12.2019 13:02

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

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

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

Другой пример без мусора: ", TYPE) .value ('(./ text ()) [1]', 'VARCHAR (MAX)')"

WITH t AS (
    SELECT 1 n, 1 g, 1 v
    UNION ALL 
    SELECT 2 n, 1 g, 2 v
    UNION ALL 
    SELECT 3 n, 2 g, 3 v
)
SELECT g
        , STUFF (
                (
                    SELECT ', ' + CAST(v AS VARCHAR(MAX))
                    FROM t sub_t
                    WHERE sub_t.g = main_t.g
                    FOR XML PATH('')
                )
                , 1, 2, ''
        ) cg
FROM t main_t
GROUP BY g

Ввод-вывод есть

*************************   ->  *********************
*   n   *   g   *   v   *       *   g   *   cg      *
*   -   *   -   *   -   *       *   -   *   -       *
*   1   *   1   *   1   *       *   1   *   1, 2    *
*   2   *   1   *   2   *       *   2   *   3       *
*   3   *   2   *   3   *       *********************
*************************   

Использование оператора Stuff and for xml path для объединения строк в строку: Group By two columns ->

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 (1,'B',5)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

-- retrieve each unique id and name columns and concatonate the values into one column
SELECT 
  [ID], 
  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES EACH APPLICATION : VALUE SET      
    FROM #YourTable 
    WHERE (ID = Results.ID and Name = results.[name] ) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID


SELECT 
  [ID],[Name] , --these are acting as the group by clause
  STUFF((
    SELECT ', '+  CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES THE VALUES FOR EACH ID NAME COMBINATION 
    FROM #YourTable 
    WHERE (ID = Results.ID and Name = results.[name] ) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS  NameValues
FROM #YourTable Results
GROUP BY ID, name

DROP TABLE #YourTable

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

  Declare @IdxList as Table(id int, choices varchar(max),AisName varchar(255))
  Insert into @IdxLIst(id,choices,AisName)
  Select IdxId,''''+Max(Title)+'''',Max(Title) From [dbo].[dta_Alias] 
 where IdxId is not null group by IdxId
  Update @IdxLIst
    set choices=choices +','''+Title+''''
    From @IdxLIst JOIN [dta_Alias] ON id=IdxId And Title <> AisName
    where IdxId is not null
    Select * from @IdxList where choices like '%,%'

Другие вопросы по теме