Як використовувати 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

18
Цей тип проблеми легко вирішується на MySQL за допомогою GROUP_CONCAT()сукупної функції, але вирішити її на Microsoft SQL Server є більш незручним. Дивіться наступне запитання щодо допомоги: " Як отримати кілька записів проти одного запису на основі співвідношення? "
Білл Карвін

1
Усі, хто має обліковий запис Microsoft, повинні проголосувати за більш просте рішення щодо підключення: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff

1
Ви можете використовувати агрегати SQLCLR, знайдені тут, як замінник, поки T-SQL не буде покращено: groupconcat.codeplex.com
Орландо Коламаттео

Відповіді:


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, яку я бачив у своєму житті. Будь-яка ідея, якщо це "швидко" для великих наборів даних? Він не починає повзати так, як курсор чи щось таке, чи не так? Я б хотів, щоб більше людей проголосували за це божевілля.
користувач12861

6
Е-е. Я просто ненавиджу стиль підзапиту. ПРИЄДНАЙТЕСЬ так приємніше. Просто не думаю, що я можу це використати у цьому рішенні. У всякому разі, я радий бачити, що тут, крім мене, є інші домівки SQL, які люблять вивчати такі речі. Кудо всім вам :)
Кевін Ферчільд

6
Трохи більш чистий спосіб зробити маніпуляцію з рядком: STUFF ((SELECT ',' + [Ім'я] + ':' + CAST ([Значення] ЯК ВАРХАР (МАКС)) З # СВОЄМО ТЕБУ, КОГО (ID = Результати.ID) ДЛЯ XML PATH ('')), 1,2, '') AS NameValues
Джонатан

3
Просто зазначити щось, що я знайшов. Навіть у випадку нечутливого до обставин,.. Значення запиту НЕОБХІДНІ. Я здогадуюсь це тому, що це XML,
залежно від

136

Якщо це SQL Server 2017 або SQL Server Vnext, SQL Azure, ви можете використовувати string_agg, як показано нижче:

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

Працює бездоганно!
argoo

1
Це чудово працює, краще, ніж прийнята відповідь.
Jannick Breunis

51

використання шляху XML не буде ідеально об'єднаним, як ви могли очікувати ... він замінить "&" на "& amp;" і також поплутаєтесь <" and "> ... можливо, ще кілька речей, не впевнені ... але ви можете спробувати це

Я натрапив на вирішення цього питання ... вам потрібно замінити:

FOR XML PATH('')
)

з:

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

... або NVARCHAR(MAX)якщо це те, що ти використовуєш.

чому чорт не SQLмає сполученої сукупної функції? це ПДФА.


2
Я розшукував мережу, шукаючи найкращий спосіб НЕ кодувати вихід. Дуже дякую! Це остаточна відповідь - поки MS не додасть належної підтримки для цього, як функція агрегації CONCAT (). Що я роблю, це кинути це у зовнішнє застосування, яке повертає моє з'єднане поле. Я не прихильник додавання вкладених виділень до моїх операцій 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

Дякую за вклад, я завжди вважаю за краще використовувати 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", і він працює дуже добре. Я настійно рекомендую це, коли функції SQL CLR є опцією.
AFract

12

SQL Server 2005 і пізніші версії дозволяють створювати власні власні функції сукупності , у тому числі для таких речей, як конкатенація - див. Зразок у нижній частині пов'язаної статті.


4
На жаль, це вимагає (?) Використання збірок CLR .. з чим ще вирішуються проблеми: - /

1
Просто в цьому прикладі використовується CLR для реальної реалізації конкатенації, але це не потрібно. Ви можете змусити функцію сукупності конкатенації використовувати FOR XML, так що принаймні акуратніше називати це в майбутньому!
Шив

12

Через вісім років ... Microsoft SQL Server vNext Database Engine нарешті покращив Transact-SQL для прямої підтримки згрупованого об'єднання рядків. Технічний перегляд спільноти версії 1.0 додав функцію STRING_AGG, а CTP 1.1 додав пункт FORIN 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 

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.
користувач12861

13
Я розумію. Але я шукав те саме, що і для Oracle, тому думав, що віддам його сюди іншим людям, як я :)
Michal B.

@MichalB. Ви не пропускаєте синтаксис в межах? наприклад: listagg (тип, ',') у групі (порядок за назвою)?
Григорій

@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=[whatever питання]
Цзюньчень Лю

7

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


11
Угруповання - це парадне відображення? Існує безліч дійсних сценаріїв для об'єднання одного стовпця в згрупований набір результатів.
MGOwen

5

Не потрібен курсор ... циклу в той час достатньо.

------------------------------
-- 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
Не використовуючи значення, ми можемо зіткнутися з проблемами, коли текст є кодованим символом 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

Використання функції Замінити та ДЛЯ ДЖОНСЬКОЇ ПАТИ

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

Для зразкових даних та інших способів натисніть тут


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.