Bagaimana cara menggunakan GROUP BY untuk merangkai string dalam SQL Server?

373

Bagaimana saya mendapatkan:

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

untuk

id          Column
1          A:4, B:8
2          C:9
Eldila
sumber
18
Jenis masalah ini diselesaikan dengan mudah pada MySQL dengan GROUP_CONCAT()fungsi agregatnya, tetapi menyelesaikannya pada Microsoft SQL Server lebih canggung. Lihat pertanyaan SO berikut untuk mendapatkan bantuan: " Bagaimana cara mendapatkan beberapa catatan terhadap satu catatan berdasarkan hubungan? "
Bill Karwin
1
Setiap orang dengan akun microsoft harus memilih solusi yang lebih sederhana untuk terhubung: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff
1
Anda dapat menggunakan Agregat SQLCLR yang ditemukan di sini sebagai pengganti sampai T-SQL ditingkatkan: groupconcat.codeplex.com
Orlando Colamatteo

Jawaban:

550

Tidak diperlukan CURSOR, WHILE loop, atau Function-Defined User .

Hanya perlu berkreasi dengan FOR XML dan PATH.

[Catatan: Solusi ini hanya berfungsi pada SQL 2005 dan yang lebih baru. Pertanyaan asli tidak menentukan versi yang digunakan.]

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
Kevin Fairchild
sumber
6
mengapa orang nolock meja temp?
Amy B
3
Ini adalah hal SQL paling keren yang pernah saya lihat dalam hidup saya. Adakah ide jika "cepat" untuk set data besar? Itu tidak mulai merangkak seperti kursor atau apa, kan? Saya berharap lebih banyak orang memilih kegilaan ini.
user12861
6
Eh. Saya hanya membenci gaya sub-permintaan itu. BERGABUNG jauh lebih baik. Tapi jangan berpikir saya bisa menggunakannya dalam solusi ini. Bagaimanapun, saya senang melihat ada dorks SQL lain di sini selain saya yang suka belajar hal-hal seperti ini. Kudos untuk kalian semua :)
Kevin Fairchild
6
Cara yang sedikit lebih bersih dalam melakukan manipulasi string: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) DARI #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')), 1,2, '') AS NameValues
Jonathan Sayce
3
Hanya untuk mencatat sesuatu yang saya temukan. Bahkan dalam lingkungan yang tidak sensitif, nilai. Bagian dari permintaan MEMBUTUHKAN menjadi huruf kecil. Saya menduga ini karena XML, yang merupakan case-sensitive
Jaloopa
136

Jika SQL Server 2017 atau SQL Server Vnext, SQL Azure Anda dapat menggunakan string_agg seperti di bawah ini:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id
Kannan Kandasamy
sumber
Bekerja tanpa cacat!
argoo
1
Ini bekerja dengan baik, lebih baik daripada jawaban yang diterima.
Jannick Breunis
51

menggunakan jalur XML tidak akan digabungkan sempurna seperti yang Anda harapkan ... itu akan menggantikan "&" dengan "& amp;" dan juga akan mengacaukan <" and "> ... mungkin beberapa hal lain, tidak yakin ... tetapi Anda dapat mencoba ini

Saya menemukan solusi untuk ini ... Anda perlu mengganti:

FOR XML PATH('')
)

dengan:

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

... atau NVARCHAR(MAX)jika itu yang Anda gunakan.

kenapa sih tidak SQLmemiliki fungsi agregat gabungan? ini adalah PITA.

Allen
sumber
2
Saya telah menjelajahi internet mencari cara terbaik untuk TIDAK mengkodekan output. Terima kasih banyak! Ini adalah jawaban yang pasti - sampai MS menambahkan dukungan yang tepat untuk ini, seperti fungsi agregat CONCAT (). Apa yang saya lakukan adalah membuang ini ke Outer-Apply yang mengembalikan bidang gabungan saya. Saya bukan penggemar menambahkan seleksi-bersarang ke dalam pernyataan pilih saya.
MikeTeeVee
Saya setuju, tanpa menggunakan Value, kita bisa mengalami masalah di mana teks adalah karakter XML yang dikodekan. Silakan temukan blog saya yang mencakup skenario untuk rangkaian dikelompokkan dalam SQL server. blog.vcillusion.co.in/…
vCillusion
40

Aku berlari ke dalam beberapa masalah ketika saya mencoba mengkonversi saran Kevin Fairchild untuk bekerja dengan string yang mengandung spasi dan karakter XML khusus ( &, <, >) yang dikodekan.

Versi terakhir dari kode saya (yang tidak menjawab pertanyaan asli tetapi mungkin bermanfaat bagi seseorang) terlihat seperti ini:

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

Daripada menggunakan spasi sebagai pembatas dan mengganti semua spasi dengan koma, itu hanya pra-pends tanda koma dan ruang untuk setiap nilai kemudian gunakan STUFFuntuk menghapus dua karakter pertama.

Pengkodean XML dilakukan secara otomatis dengan menggunakan arahan TYPE .

Jonathan Sayce
sumber
21

Opsi lain menggunakan Sql Server 2005 dan di atasnya

---- 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
sumber
Terima kasih atas masukannya, saya selalu lebih suka menggunakan CTE dan CTE rekursif untuk menyelesaikan masalah di SQL server. Ini berhasil satu bekerja untuk saya hebat!
gbdavid
apakah mungkin untuk menggunakannya dalam kueri dengan penerapan luar?
Api di lubang
14

Instal Agregat SQLCLR dari http://groupconcat.codeplex.com

Kemudian Anda dapat menulis kode seperti ini untuk mendapatkan hasil yang Anda minta:

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;
Orlando Colamatteo
sumber
Saya menggunakannya beberapa tahun yang lalu, sintaksnya jauh lebih bersih daripada semua trik "XML Path" dan itu bekerja dengan sangat baik. Saya sangat merekomendasikannya ketika fungsi SQL CLR adalah pilihan.
AFract
12

SQL Server 2005 dan yang lebih baru memungkinkan Anda untuk membuat fungsi agregat kustom Anda sendiri , termasuk untuk hal-hal seperti penggabungan - lihat contoh di bagian bawah artikel yang ditautkan.

Joel Coehoorn
sumber
4
Sayangnya ini membutuhkan (?) Menggunakan majelis CLR .. yang merupakan masalah lain untuk berurusan dengan: - /
1
Contohnya saja menggunakan CLR untuk implementasi rangkaian aktual tetapi ini tidak diperlukan. Anda dapat membuat fungsi agregat gabungan menggunakan UNTUK XML sehingga setidaknya lebih baik untuk menyebutnya di masa depan!
Shiv
12

Delapan tahun kemudian ... Mesin Database Microsoft SQL Server vNext akhirnya meningkatkan Transact-SQL untuk secara langsung mendukung penggabungan string yang dikelompokkan. Komunitas Technical Preview versi 1.0 menambahkan fungsi STRING_AGG dan CTP 1.1 menambahkan klausa WITHIN GROUP untuk fungsi STRING_AGG.

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

Semir Sargent
sumber
9

Ini hanya tambahan untuk posting Kevin Fairchild (sangat pintar). Saya ingin menambahkannya sebagai komentar, tetapi saya belum memiliki poin yang cukup :)

Saya menggunakan ide ini untuk tampilan yang sedang saya kerjakan, namun item yang saya gabungkan mengandung ruang. Jadi saya memodifikasi sedikit kode untuk tidak menggunakan spasi sebagai pembatas.

Sekali lagi terima kasih atas solusi kerennya 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 
Phillip
sumber
9

Contohnya adalah

Di Oracle Anda dapat menggunakan fungsi agregat LISTAGG.

Catatan asli

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

Sql

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

Hasil dalam

name   type
------------
name1  type1
name2  type2; type3
Michal B.
sumber
6
Terlihat bagus, tetapi pertanyaannya bukan tentang Oracle.
user12861
13
Saya mengerti. Tapi saya sedang mencari hal yang sama untuk Oracle, jadi saya pikir saya akan meletakkannya di sini untuk orang lain seperti saya :)
Michal B.
@MichalB. Apakah Anda tidak kehilangan sintaksis dalam? misalnya: listagg (ketik, ',') di dalam grup (diurutkan berdasarkan nama)?
gregory
@regregory: Saya mengedit jawaban saya. Saya pikir solusi lama saya dulu bekerja kembali pada hari-hari. Formulir saat ini yang Anda sarankan akan berfungsi pasti, terima kasih.
Michal B.
1
untuk orang-orang masa depan - Anda dapat menulis pertanyaan baru dengan jawaban Anda sendiri untuk perbedaan yang signifikan seperti platform yang berbeda
Mike M
7

Pertanyaan semacam ini sering ditanyakan di sini, dan solusinya akan sangat bergantung pada persyaratan yang mendasarinya:

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

dan

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

Biasanya, tidak ada cara hanya SQL untuk melakukan ini tanpa sql dinamis, fungsi yang ditentukan pengguna, atau kursor.

Cade Roux
sumber
2
Tidak benar. Solusi cyberkiwi menggunakan cte: s adalah sql murni tanpa peretasan khusus vendor.
Björn Lindqvist
1
Pada saat tanya jawab, saya tidak akan menganggap CTE rekursif sebagai sangat portabel, tetapi sekarang didukung oleh Oracle. Solusi terbaik akan tergantung pada platform. Untuk SQL Server kemungkinan besar teknik XML UNTUK atau agregat CLR pelanggan.
Cade Roux
1
jawaban akhir untuk semua pertanyaan? stackoverflow.com/search?q=[apa pun pertanyaannya]
Junchen Liu
7

Hanya untuk menambah apa yang dikatakan Cade, ini biasanya tampilan depan dan karenanya harus ditangani di sana. Saya tahu bahwa kadang-kadang lebih mudah untuk menulis sesuatu 100% dalam SQL untuk hal-hal seperti ekspor file atau solusi "SQL saja" lainnya, tetapi sebagian besar waktu penggabungan ini harus ditangani di lapisan tampilan Anda.

Tom H
sumber
11
Pengelompokan adalah hal tampilan front-end sekarang? Ada banyak skenario yang valid untuk menggabungkan satu kolom di set hasil yang dikelompokkan.
MGOwen
5

Tidak perlu kursor ... loop sementara sudah cukup.

------------------------------
-- 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
Amy B
sumber
@marc_s mungkin kritik yang lebih baik adalah bahwa PRIMARY KEY harus dideklarasikan pada variabel tabel.
Amy B
@marc_s Pada pemeriksaan lebih lanjut, artikel itu palsu - seperti hampir semua diskusi kinerja tanpa pengukuran IO. Saya memang belajar tentang LAG - jadi terima kasih untuk itu.
Amy B
4

Mari kita menjadi sangat sederhana:

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

Ganti baris ini:

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

Dengan kueri Anda.

Marquinho Peli
sumber
3

tidak melihat jawaban lintas diterapkan, juga tidak perlu ekstraksi xml. Ini adalah versi yang sedikit berbeda dari apa yang ditulis Kevin Fairchild. Lebih cepat dan lebih mudah digunakan dalam permintaan yang lebih kompleks:

   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
Mordechai
sumber
1
Tanpa menggunakan Value, kita bisa mengalami masalah di mana teks adalah karakter yang dikodekan XML
vCillusion
2

Anda dapat meningkatkan kinerja yang signifikan dengan cara berikut jika grup dengan sebagian besar berisi satu item:

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
Eduard
sumber
Dengan asumsi Anda tidak ingin nama duplikat dalam daftar, yang Anda mungkin atau mungkin tidak.
jnm2
1

Menggunakan Ganti Fungsi dan UNTUK PATUNG JSON

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

Untuk sampel data dan lebih banyak cara klik di sini

Mahesh
sumber
1

Jika Anda telah mengaktifkan CLR Anda bisa menggunakan pustaka Group_Concat dari GitHub

Manfred Wippel
sumber