Làm cách nào để sử dụng GROUP BY để nối chuỗi trong SQL Server?


373

Làm sao tôi có thể lấy:

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

đến

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

18
Loại vấn đề này được giải quyết dễ dàng trên MySQL với GROUP_CONCAT()chức năng tổng hợp của nó , nhưng việc giải quyết nó trên Microsoft SQL Server khó xử hơn. Xem câu hỏi SO sau đây để được trợ giúp: " Làm thế nào để có được nhiều bản ghi so với một bản ghi dựa trên mối quan hệ? "
Bill Karwin

1
Mọi người có tài khoản microsoft nên bỏ phiếu cho một giải pháp đơn giản hơn về kết nối: connect.microsoft.com/QueryServer/feedback/details/427987/ Kẻ
Jens Mühlenhoff

1
Bạn có thể sử dụng các Tập hợp SQLCLR được tìm thấy ở đây để thay thế cho đến khi T-SQL được tăng cường: groupconcat.codeplex.com
Orlando Colamatteo 19/03/2016

Câu trả lời:


550

Không cần vòng lặp CURSOR, WHILE hoặc Hàm do người dùng xác định .

Chỉ cần sáng tạo với FOR XML và PATH.

[Lưu ý: Giải pháp này chỉ hoạt động trên SQL 2005 trở lên. Câu hỏi ban đầu không chỉ định phiên bản đang sử dụng.]

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
Tại sao một người nolock một bảng tạm thời?
Amy B

3
Đây là điều SQL thú vị nhất tôi từng thấy trong đời. Bất cứ ý tưởng nếu nó "nhanh" cho các tập dữ liệu lớn? Nó không bắt đầu bò như con trỏ hay bất cứ thứ gì, phải không? Tôi muốn nhiều người sẽ bỏ phiếu điên rồ này lên.
dùng12861

6
Hở. Tôi chỉ ghét phong cách truy vấn phụ của nó. THAM GIA đẹp hơn rất nhiều. Chỉ cần đừng nghĩ rằng tôi có thể sử dụng nó trong giải pháp này. Nhưng dù sao, tôi rất vui khi thấy có những người chơi SQL khác ở đây ngoài tôi, những người thích học những thứ như thế này. Kudos cho tất cả các bạn :)
Kevin Fairchild

6
Một cách gọn gàng hơn để thực hiện thao tác chuỗi: STUFF ((CHỌN ',' + [Tên] + ':' + CAST ([Giá trị] NHƯ VARCHAR (MAX)) TỪ #YourTable WHERE (ID = results.ID) CHO XML PATH ('')), 1,2, '') NHƯ TênValues
Jonathan Sayce

3
Chỉ cần lưu ý một cái gì đó tôi đã tìm thấy. Ngay cả trong môi trường không phân biệt chữ hoa chữ thường, phần .value của truy vấn CẦN phải là chữ thường. Tôi đoán điều này là do nó là XML, phân biệt chữ hoa chữ thường
Jaloopa

136

Nếu là SQL Server 2017 hoặc SQL Server Vnext, SQL Azure, bạn có thể sử dụng chuỗi_agg như dưới đây:

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

Công trình hoàn hảo!
argoo

1
Điều này làm việc tuyệt vời, tốt hơn sau đó câu trả lời được chấp nhận.
Jannick Breunis

51

sử dụng đường dẫn XML sẽ không kết hợp hoàn hảo như bạn mong đợi ... nó sẽ thay thế "&" bằng "& amp;" và cũng sẽ gây rối với <" and "> ... có thể một vài thứ khác, không chắc chắn ... nhưng bạn có thể thử điều này

Tôi đã gặp một cách giải quyết cho việc này ... bạn cần thay thế:

FOR XML PATH('')
)

với:

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

... Hoặc NVARCHAR(MAX)nếu đó là những gì bạn đang sử dụng.

Tại sao địa ngục không SQLcó chức năng tổng hợp nối? đây là một Pita.


2
Tôi đã lùng sục trên mạng để tìm cách tốt nhất để KHÔNG mã hóa đầu ra. Cảm ơn bạn rất nhiều! Đây là câu trả lời dứt khoát - cho đến khi MS bổ sung hỗ trợ thích hợp cho việc này, giống như hàm tổng hợp CONCAT (). Những gì tôi làm là ném cái này vào Ứng dụng ngoài để trả về trường được nối của tôi. Tôi không phải là người thích thêm các lựa chọn lồng nhau vào các câu lệnh chọn của mình.
MikeTeeVee

Tôi đồng ý, không sử dụng Giá trị, chúng ta có thể gặp phải các vấn đề trong đó văn bản là ký tự được mã hóa XML. Vui lòng tìm blog của tôi bao gồm các kịch bản để ghép nhóm trong máy chủ SQL. blog.vcillusion.co.in/ từ
vCillusion

40

Tôi chạy vào một vài vấn đề khi tôi đã cố gắng chuyển đổi đề nghị Kevin Fairchild để làm việc với các chuỗi có chứa khoảng trắng và ký tự đặc biệt XML ( &, <, >) được mã hóa.

Phiên bản cuối cùng của mã của tôi (không trả lời câu hỏi ban đầu nhưng có thể hữu ích cho ai đó) trông như thế này:

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

Thay vì sử dụng một khoảng trắng làm dấu phân cách và thay thế tất cả các khoảng trắng bằng dấu phẩy, nó chỉ sử dụng trước một dấu phẩy và khoảng trắng cho mỗi giá trị sau đó sử dụng STUFFđể xóa hai ký tự đầu tiên.

Mã hóa XML được chăm sóc tự động bằng cách sử dụng lệnh TYPE .


21

Một tùy chọn khác sử dụng Sql Server 2005 trở lên

---- 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

Cảm ơn về đầu vào, tôi luôn thích sử dụng CTE và CTE đệ quy để giải quyết các vấn đề trong máy chủ SQL. Đây là một công việc tuyệt vời cho tôi!
gbdavid

Có thể sử dụng nó trong một truy vấn với áp dụng bên ngoài?
bắn vào lỗ

14

Cài đặt các Tập hợp SQLCLR từ http://groupconcat.codeplex.com

Sau đó, bạn có thể viết mã như thế này để nhận được kết quả mà bạn yêu cầu:

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;

Tôi đã sử dụng nó vài năm trước, cú pháp sạch hơn nhiều so với tất cả các thủ thuật "Đường dẫn XML" và nó hoạt động rất tốt. Tôi thực sự khuyên bạn nên sử dụng khi các hàm SQL CLR là một tùy chọn.
Thu hút

12

SQL Server 2005 trở lên cho phép bạn tạo các hàm tổng hợp tùy chỉnh của riêng mình , bao gồm cả những thứ như ghép nối - xem mẫu ở cuối bài viết được liên kết.


4
Thật không may, điều này đòi hỏi (?) Khi sử dụng các cụm CLR .. đó là một vấn đề khác cần giải quyết: - /

1
Chỉ ví dụ sử dụng CLR để thực hiện nối ghép thực tế nhưng điều này là không bắt buộc. Bạn có thể làm cho hàm tổng hợp nối sử dụng FOR XML để ít nhất nó sẽ gọn gàng hơn để gọi nó trong tương lai!
Shiv

12

Tám năm sau ... Microsoft SQL Server vNext Database Engine cuối cùng đã tăng cường Transact-SQL để hỗ trợ trực tiếp nối chuỗi được nhóm. Phiên bản Xem trước Kỹ thuật Cộng đồng 1.0 đã thêm chức năng STRING_AGG và CTP 1.1 đã thêm mệnh đề FORIN GROUP cho chức năng STRING_AGG.

Tham khảo: https://msdn.microsoft.com/en-us/l Library / mt775028.aspx


9

Đây chỉ là một bổ sung cho bài viết của Kevin Fairchild (rất thông minh) Tôi đã có thể thêm nó dưới dạng một nhận xét, nhưng tôi chưa có đủ điểm :)

Tôi đã sử dụng ý tưởng này cho một khung nhìn mà tôi đang thực hiện, tuy nhiên các mục tôi đang kết hợp các không gian chứa. Vì vậy, tôi đã sửa đổi mã một chút để không sử dụng khoảng trắng làm dấu phân cách.

Một lần nữa cảm ơn vì cách giải quyết tuyệt vời 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 

9

Một ví dụ sẽ là

Trong Oracle, bạn có thể sử dụng hàm tổng hợp LISTAGG.

Hồ sơ gốc

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

Sql

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

Kết quả là

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

6
Có vẻ tốt, nhưng các câu hỏi đặc biệt không phải về Oracle.
dùng12861

13
Tôi hiểu. Nhưng tôi đang tìm kiếm điều tương tự cho Oracle, vì vậy tôi nghĩ rằng tôi sẽ đặt nó ở đây cho những người khác như tôi :)
Michal B.

@MichalB. Bạn không thiếu cú ​​pháp trong? ví dụ: listagg (loại, ',') trong nhóm (thứ tự theo tên)?
phái

@gregory: Tôi đã chỉnh sửa câu trả lời của mình. Tôi nghĩ rằng giải pháp cũ của tôi được sử dụng để làm việc trở lại trong những ngày. Hình thức hiện tại mà bạn đề xuất sẽ hoạt động chắc chắn, cảm ơn.
Michal B.

1
cho những người trong tương lai - bạn có thể viết một câu hỏi mới với câu trả lời của riêng bạn cho một sự khác biệt đáng kể như nền tảng khác
Mike M

7

Loại câu hỏi này được hỏi ở đây rất thường xuyên, và giải pháp sẽ phụ thuộc rất nhiều vào các yêu cầu cơ bản:

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

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

Thông thường, không có cách nào chỉ có SQL để thực hiện việc này mà không có sql động, hàm do người dùng xác định hoặc con trỏ.


2
Không đúng. giải pháp của cyberkiwi sử dụng cte: s là sql thuần túy mà không có bất kỳ tin tặc nào của nhà cung cấp.
Bjorn Lindqvist

1
Tại thời điểm của câu hỏi và câu trả lời, tôi sẽ không tính các CTE đệ quy là di động khủng khiếp, nhưng giờ đây chúng được Oracle hỗ trợ. Giải pháp tốt nhất là phụ thuộc vào nền tảng. Đối với SQL Server, rất có thể là kỹ thuật FOR XML hoặc tổng hợp CLR của khách hàng.
Cade Roux

1
câu trả lời cuối cùng cho tất cả các câu hỏi? stackoverflow.com/search?q=[whwh câu hỏi]
Junchen Liu

7

Chỉ cần thêm vào những gì Cade nói, đây thường là một thứ hiển thị mặt trước và do đó nên được xử lý ở đó. Tôi biết rằng đôi khi việc viết một cái gì đó 100% bằng SQL dễ dàng hơn cho những thứ như xuất tệp hoặc các giải pháp "chỉ SQL" khác, nhưng hầu hết các lần ghép này nên được xử lý trong lớp hiển thị của bạn.


11
Nhóm là một điều hiển thị phía trước bây giờ? Có rất nhiều kịch bản hợp lệ để nối một cột trong tập kết quả được nhóm.
MGOwen

5

Đừng cần một con trỏ ... một vòng lặp while là đủ.

------------------------------
-- 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 có lẽ một lời chỉ trích tốt hơn là PRIMary KEY nên được khai báo trên các biến của bảng.
Amy B

@marc_s Khi kiểm tra thêm, bài báo đó là một sự giả tạo - vì hầu như tất cả các cuộc thảo luận về hiệu suất mà không cần đo IO. Tôi đã học về LAG - vì vậy cảm ơn vì điều đó.
Amy B

4

Chúng ta hãy làm rất đơn giản:

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

Thay thế dòng này:

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

Với truy vấn của bạn.


3

không thấy bất kỳ câu trả lời áp dụng chéo nào, cũng không cần trích xuất xml. Đây là một phiên bản hơi khác với những gì Kevin Fairchild đã viết. Nó nhanh hơn và dễ sử dụng hơn trong các truy vấn phức tạp hơn:

   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
Không sử dụng Giá trị, chúng ta có thể gặp phải các vấn đề trong đó văn bản là ký tự được mã hóa XML
vCillusion

2

Bạn có thể cải thiện hiệu suất đáng kể theo cách sau nếu nhóm bằng cách chứa hầu hết một mục:

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

Giả sử bạn không muốn tên trùng lặp trong danh sách, mà bạn có thể hoặc không thể.
jnm2

1

Sử dụng chức năng thay thế và cho 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

Đối với dữ liệu mẫu và nhiều cách khác bấm vào đây


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.