วิธีใช้ 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 นั้นน่าอึดอัดใจมากขึ้น ดูคำถาม SO ต่อไปนี้เพื่อขอความช่วยเหลือ: " วิธีรับหลายระเบียนต่อหนึ่งระเบียนตามความสัมพันธ์ได้อย่างไร "
Bill Karwin

1
ทุกคนที่มีบัญชี microsoft ควรลงคะแนนเพื่อหาวิธีแก้ปัญหาที่ง่ายกว่าในการเชื่อมต่อ: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff

1
คุณสามารถใช้ SQLCLR Aggregates ที่นี่แทนได้จนกว่า T-SQL จะได้รับการปรับปรุง: groupconcat.codeplex.com
Orlando Colamatteo

คำตอบ:


550

ไม่มีเคอร์เซอร์ห่วงขณะหรือฟังก์ชั่นที่ผู้ใช้กำหนดความจำเป็น

เพียงแค่ต้องมีความคิดสร้างสรรค์ด้วย 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
ทำไมคนคนหนึ่งจะทำล๊อคโต๊ะอุณหภูมิ
Amy B

3
นี่คือสิ่ง SQL ที่ยอดเยี่ยมที่สุดที่ฉันเคยเห็นในชีวิตของฉัน มีความคิดใดบ้างไหมถ้าเป็น "เร็ว" สำหรับชุดข้อมูลขนาดใหญ่ มันไม่ได้เริ่มคลานเหมือนที่เคอร์เซอร์ต้องการหรืออะไรก็ตาม ฉันหวังว่าผู้คนมากขึ้นจะลงคะแนนความบ้าคลั่งนี้ขึ้น
user12861

6
เอ๊ะ ฉันเกลียดสไตล์คิวรีย่อยของมัน เข้าร่วมดีกว่ามาก แค่คิดว่าฉันไม่สามารถใช้ประโยชน์จากวิธีนี้ได้ ยังไงก็ตามฉันดีใจที่เห็นว่ามี SQL อื่น ๆ อยู่ที่นี่นอกเหนือจากฉันที่ชอบเรียนรู้สิ่งนี้ ขอชื่นชมทุกท่าน :)
Kevin Fairchild

6
วิธีทำความสะอาดการจัดการสตริง: STUFF ((SELECT ',' + [ชื่อ] + ':' + CAST ([ค่า] AS VARCHAR (MAX)) จาก #YourTable WHERE (ID = Results.ID) สำหรับ XML PATH ('')), 1,2, '') AS NameValues
Jonathan Sayce

3
เพียงสังเกตสิ่งที่ฉันพบ แม้ในสภาพแวดล้อมที่ไม่คำนึงถึงขนาดตัวพิมพ์ส่วน. value ของเคียวรี NEEDS จะเป็นตัวพิมพ์เล็ก ฉันเดาว่าเป็นเพราะมันเป็น XML ซึ่งเป็นตัวพิมพ์เล็กและตัวพิมพ์ใหญ่
Jaloopa

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ฟังก์ชันการรวมเข้าด้วยกัน? นี่คือ PITA


2
ฉันได้พยายามค้นหาวิธีที่ดีที่สุดในการไม่เข้ารหัสเอาต์พุต ขอบคุณมาก! นี่คือคำตอบที่ชัดเจน - จนกระทั่ง MS เพิ่มการสนับสนุนที่เหมาะสมสำหรับสิ่งนี้เช่นฟังก์ชัน CONCAT () รวม สิ่งที่ฉันทำคือโยนสิ่งนี้ลงใน Outer-Apply ซึ่งจะคืนค่าฟิลด์ที่ต่อกันของฉัน ฉันไม่ใช่แฟนตัวยงของการเพิ่มตัวเลือกที่ซ้อนอยู่ในคำสั่งที่เลือกของฉัน
MikeTeeVee

ฉันตกลงโดยไม่ใช้ Value เราสามารถพบปัญหาที่ข้อความเป็นอักขระที่เข้ารหัส XML โปรดหาบล็อกของฉันครอบคลุมสถานการณ์สำหรับการเชื่อมต่อแบบกลุ่มในเซิร์ฟเวอร์ SQL blog.vcillusion.co.in/…
vCillusion

40

ฉันวิ่งเข้าไปในคู่ของปัญหาที่เกิดขึ้นเมื่อฉันพยายามแปลงข้อเสนอแนะของเควินแฟร์ไชลด์ของการทำงานกับสตริงที่มีช่องว่างและตัวอักษร 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 Aggregates จาก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 เป็นตัวเลือก
หยุด

12

SQL Server 2005 และใหม่กว่าอนุญาตให้คุณสร้างฟังก์ชันการรวมแบบกำหนดเองของคุณเองรวมถึงสิ่งต่าง ๆ เช่นการต่อข้อมูล - ดูตัวอย่างที่ด้านล่างของบทความที่เชื่อมโยง


4
น่าเสียดายที่ต้องใช้ (?) โดยใช้ชุดประกอบ CLR .. ซึ่งเป็นอีกปัญหาที่ต้องจัดการกับ: - /

1
ตัวอย่างนี้ใช้ CLR สำหรับการปรับใช้การต่อข้อมูลจริง แต่ไม่จำเป็น คุณสามารถทำให้ฟังก์ชั่นการรวมแบบเรียงต่อกันใช้สำหรับ XML ดังนั้นอย่างน้อยก็เป็นตัวเรียกที่จะเรียกใช้ในอนาคต!
ชีฟ

12

แปดปีต่อมา ... Microsoft SQL Server vNext Database Engine ได้ปรับปรุง Transact-SQL ในที่สุดเพื่อรองรับการจัดเรียงสตริงที่จัดกลุ่มโดยตรง Community Technical Preview เวอร์ชัน 1.0 เพิ่มฟังก์ชัน STRING_AGG และ CTP 1.1 เพิ่มส่วนคำสั่ง WITHIN GROUP สำหรับฟังก์ชัน STRING_AGG

การอ้างอิง: https://msdn.microsoft.com/en-us/library/mt775028.aspx


9

นี่เป็นเพียงส่วนเสริมของโพสต์ของ Kevin Fairchild (ฉลาดมากโดยวิธี) ฉันจะเพิ่มมันเป็นความคิดเห็น แต่ฉันยังมีคะแนนไม่พอ :)

ฉันกำลังใช้ความคิดนี้สำหรับมุมมองที่ฉันกำลังทำงานอยู่ แต่สิ่งที่ฉันกำลัง concatinating ช่องว่างที่มี ดังนั้นฉันจึงแก้ไขโค้ดเล็กน้อยเพื่อไม่ให้มีช่องว่างเป็นตัวคั่น

ขอบคุณอีกครั้งสำหรับวิธีแก้ปัญหาที่ยอดเยี่ยมของ 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

ตัวอย่างจะเป็น

ใน 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
ฉันเข้าใจ. แต่ผมกำลังมองหาสิ่งเดียวกันสำหรับ Oracle ดังนั้นฉันคิดว่าฉันจะใส่ที่นี่สำหรับคนอื่น ๆ เช่นฉัน :)
คาลบี

@MichalB คุณไม่ได้ใช้ไวยากรณ์ภายในใช่หรือไม่ เช่น: listagg (ประเภท, ',') ภายในกลุ่ม (เรียงตามชื่อ)?
กอรี่

@ รวม: ฉันแก้ไขคำตอบของฉัน ฉันคิดว่าวิธีแก้ปัญหาเก่าของฉันเคยทำงานในวันที่ แบบฟอร์มปัจจุบันที่คุณแนะนำจะใช้ได้แน่นอนขอบคุณ
มิคาลบี

1
สำหรับคนในอนาคต - คุณสามารถเขียนคำถามใหม่ด้วยคำตอบของคุณเองสำหรับความแตกต่างที่สำคัญเช่นแพลตฟอร์มที่แตกต่าง
Mike M

7

คำถามประเภทนี้ถูกถามบ่อยมากและวิธีการแก้ปัญหานั้นขึ้นอยู่กับข้อกำหนดพื้นฐานหลายประการ:

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

และ

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

โดยทั่วไปไม่มีวิธี SQL เท่านั้นที่จะทำเช่นนี้โดยไม่ต้อง sql แบบไดนามิกฟังก์ชั่นที่ผู้ใช้กำหนดหรือเคอร์เซอร์


2
ไม่จริง. โซลูชันของ cyberkiwi ที่ใช้ cte: s นั้นเป็น sql ล้วนๆ
Björn Lindqvist

1
ในช่วงเวลาของคำถามและคำตอบฉันจะไม่นับ CTE ที่เรียกซ้ำได้ว่าเป็นอุปกรณ์พกพาที่น่ากลัว แต่ตอนนี้ Oracle ได้รับการสนับสนุนแล้ว ทางออกที่ดีที่สุดจะขึ้นอยู่กับแพลตฟอร์ม สำหรับ SQL Server นั้นน่าจะเป็นเทคนิค FOR XML หรือการรวม CLR ของลูกค้า
เคด Roux

1
คำตอบสุดท้ายสำหรับคำถามทั้งหมดหรือไม่ stackoverflow.com/search?q= [คำถามใด ๆ ]
Junchen Liu

7

เพียงเพื่อเพิ่มสิ่งที่เคดกล่าวว่าโดยปกติแล้วนี่เป็นสิ่งที่แสดงผลส่วนหน้าและควรจัดการที่นั่น ฉันรู้ว่าบางครั้งการเขียนบางอย่าง 100% ใน SQL สำหรับสิ่งต่าง ๆ เช่นการส่งออกไฟล์หรือวิธีแก้ปัญหา "SQL เท่านั้น" อื่น ๆ ได้ง่ายขึ้น แต่ส่วนใหญ่การต่อข้อมูลนี้ควรถูกจัดการในเลเยอร์การแสดงผลของคุณ


11
การจัดกลุ่มเป็นส่วนแสดงผลส่วนหน้าตอนนี้หรือไม่ มีสถานการณ์จำลองที่ถูกต้องมากมายสำหรับการต่อหนึ่งคอลัมน์ในชุดผลลัพธ์ที่จัดกลุ่ม
MGOwen

5

ไม่ต้องการเคอร์เซอร์ ... a ขณะที่ลูปเพียงพอ

------------------------------
-- 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 บางทีคำวิจารณ์ที่ดีกว่าก็คือคีย์หลักควรถูกประกาศไว้ในตัวแปรตาราง
เอมี่ B

@marc_s ในการตรวจสอบเพิ่มเติมบทความนั้นเป็นสิ่งที่หลอกลวงเช่นเดียวกับการพูดคุยเรื่องประสิทธิภาพเกือบทั้งหมดโดยไม่มีการวัด IO ฉันเรียนรู้เกี่ยวกับ LAG - ขอบคุณมากสำหรับสิ่งนั้น
Amy B

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 นี่เป็นเวอร์ชั่นที่แตกต่างออกไปเล็กน้อยจากสิ่งที่ Kevin Fairchild เขียน มันใช้งานได้เร็วและง่ายขึ้นในการสืบค้นที่ซับซ้อนมากขึ้น:

   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

การใช้ฟังก์ชั่นแทนที่และสำหรับ 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

สำหรับตัวอย่างข้อมูลและวิธีการเพิ่มเติมคลิกที่นี่


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.