วิธีที่เหมาะสมที่สุดในการเชื่อมต่อ / รวมสตริง


106

ฉันกำลังหาวิธีรวมสตริงจากแถวต่างๆให้เป็นแถวเดียว ฉันต้องการทำสิ่งนี้ในสถานที่ต่างๆดังนั้นการมีฟังก์ชันเพื่ออำนวยความสะดวกก็น่าจะดี ฉันได้ลองวิธีแก้ปัญหาโดยใช้COALESCEและFOR XMLแต่ก็ไม่ได้ตัดให้ฉัน

การรวมสตริงจะทำสิ่งนี้:

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus

ฉันได้ดูฟังก์ชันการรวมที่กำหนดโดย CLRเพื่อทดแทนCOALESCEและFOR XMLแต่เห็นได้ชัดว่าSQL Azure ไม่รองรับสิ่งที่กำหนดโดย CLR ซึ่งเป็นความเจ็บปวดสำหรับฉันเพราะฉันรู้ว่าการใช้งานได้จะช่วยแก้ปัญหาได้มากมาย ปัญหาสำหรับฉัน

มีวิธีแก้ปัญหาที่เป็นไปได้หรือวิธีการที่ดีที่สุดในทำนองเดียวกัน (ซึ่งอาจไม่เหมาะสมเท่า CLR แต่เดี๋ยวก่อนฉันจะเอาสิ่งที่ฉันได้รับ) ที่ฉันสามารถใช้เพื่อรวบรวมสิ่งของของฉันได้ไหม


ไม่for xmlได้ผลสำหรับคุณในทางใด?
Mikael Eriksson

4
มันใช้งานได้ แต่ฉันดูแผนการดำเนินการและแต่ละfor xmlรายการแสดงการใช้งาน 25% ในแง่ของประสิทธิภาพการสืบค้น (แบบสอบถามจำนวนมาก!)
แมตต์

2
มีหลายวิธีในการทำfor xml pathแบบสอบถาม เร็วกว่าคนอื่น ๆ บ้าง มันอาจจะขึ้นอยู่กับข้อมูลของคุณ แต่คนที่ใช้อยู่ในประสบการณ์ของผมช้ากว่าการใช้distinct group byและถ้าคุณกำลังใช้.value('.', nvarchar(max))เพื่อรับค่าที่ต่อกันคุณควรเปลี่ยนเป็น.value('./text()[1]', nvarchar(max))
Mikael Eriksson

3
คำตอบที่ได้รับการยอมรับของคุณมีลักษณะคล้ายกับของฉันคำตอบในstackoverflow.com/questions/11137075/...ซึ่งผมคิดว่าจะเร็วกว่า XML อย่าหลงกลค่าใช้จ่ายในการค้นหาคุณต้องมีข้อมูลเพียงพอเพื่อดูว่าอันไหนเร็วกว่า XML เป็นเร็วขึ้นซึ่งเกิดขึ้นเป็น @ MikaelEriksson ของคำตอบในวันที่เดียวกันคำถาม เลือกใช้วิธี XML
Michael Buen

2
โปรดลงคะแนนสำหรับโซลูชันดั้งเดิมสำหรับสิ่งนี้ที่นี่: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

คำตอบ:


69

วิธีการแก้

คำจำกัดความของoptimalอาจแตกต่างกันไป แต่นี่คือวิธีการต่อสตริงจากแถวต่างๆโดยใช้ Transact SQL ปกติซึ่งควรทำงานได้ดีใน Azure

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM dbo.SourceTable
),
Concatenated AS
(
    SELECT 
        ID, 
        CAST(Name AS nvarchar) AS FullName, 
        Name, 
        NameNumber, 
        NameCount 
    FROM Partitioned 
    WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, 
        CAST(C.FullName + ', ' + P.Name AS nvarchar), 
        P.Name, 
        P.NameNumber, 
        P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C 
                ON P.ID = C.ID 
                AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

คำอธิบาย

วิธีการลดลงเหลือสามขั้นตอน:

  1. กำหนดหมายเลขแถวโดยใช้OVERและPARTITIONจัดกลุ่มและจัดลำดับตามความจำเป็นสำหรับการเรียงต่อกัน ผลลัพธ์คือPartitionedCTE เราจะนับจำนวนแถวในแต่ละพาร์ติชันเพื่อกรองผลลัพธ์ในภายหลัง

  2. การใช้ CTE แบบเรียกซ้ำ ( Concatenated) วนซ้ำผ่านหมายเลขแถว ( NameNumberคอลัมน์) เพื่อเพิ่มNameค่าลงในFullNameคอลัมน์

  3. กรองผลการค้นหาทั้งหมด NameNumberแต่คนที่มีมากที่สุด

โปรดทราบว่าในการทำให้คำค้นหานี้สามารถคาดเดาได้จะต้องกำหนดการจัดกลุ่มทั้งสอง (ตัวอย่างเช่นในแถวสถานการณ์ของคุณที่มีแถวเดียวกันIDจะเรียงต่อกัน) และการเรียงลำดับ (ฉันคิดว่าคุณเพียงแค่เรียงสตริงตามตัวอักษรก่อนที่จะเรียงต่อกัน)

ฉันได้ทดสอบโซลูชันอย่างรวดเร็วบน SQL Server 2012 ด้วยข้อมูลต่อไปนี้:

INSERT dbo.SourceTable (ID, Name)
VALUES 
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')

ผลการค้นหา:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks

5
ฉันตรวจสอบการใช้เวลาของวิธีนี้เทียบกับ xmlpath และฉันถึงประมาณ 4 มิลลิวินาทีเทียบกับประมาณ 54 มิลลิวินาที ดังนั้นวิธี xmplath จึงดีกว่าโดยเฉพาะในกรณีขนาดใหญ่ ฉันจะเขียนโค้ดเปรียบเทียบในคำตอบแยกต่างหาก
QMaster

จะดีกว่ามากเนื่องจากวิธีนี้ใช้ได้กับค่าสูงสุด 100 ค่าเท่านั้น
Romano Zumbé

@ romano-zumbéใช้ MAXRECURSION เพื่อตั้งค่าขีด จำกัด CTE ตามที่คุณต้องการ
Serge Belov

1
น่าแปลกที่ CTE ช้ากว่าสำหรับฉัน sqlperformance.com/2014/08/t-sql-queries/…เปรียบเทียบเทคนิคต่างๆมากมายและดูเหมือนจะเห็นด้วยกับผลลัพธ์ของฉัน
Nickolay

โซลูชันนี้สำหรับตารางที่มีมากกว่า 1 ล้านเรกคอร์ดไม่ทำงาน นอกจากนี้เรายังมีขีดจำกัดความลึกแบบวนซ้ำ
Ardalan Shahgholi

52

วิธีการใช้ FOR XML PATH เหมือนด้านล่างนี้ช้าจริงหรือ? Itzik Ben-Gan เขียนว่าวิธีนี้มีประสิทธิภาพที่ดีในหนังสือ Querying T-SQL ของเขา (Mr.

create table #t (id int, name varchar(20))

insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')

select  id
        ,Names = stuff((select ', ' + name as [text()]
        from #t xt
        where xt.id = t.id
        for xml path('')), 1, 2, '')
from #t t
group by id

อย่าลืมใส่ดัชนีในidคอลัมน์นั้นเมื่อขนาดของตารางมีปัญหา
milivojeviCH

2
และหลังจากอ่านวิธีการทำงานของ Stuff / สำหรับเส้นทาง xml ( stackoverflow.com/a/31212160/1026 ) ฉันมั่นใจว่าเป็นโซลูชันที่ดีแม้จะมี XML ในชื่อ :)
Nickolay

1
@slackterman ขึ้นอยู่กับจำนวนบันทึกที่จะดำเนินการ ฉันคิดว่า XML มีข้อบกพร่องในจำนวนที่ต่ำเมื่อเทียบกับ CTE แต่ที่ระดับเสียงด้านบนจะช่วยลดข้อ จำกัด ของ Recursion Dept และนำทางได้ง่ายขึ้นหากทำอย่างถูกต้องและรวบรัด
GoldBishop

สำหรับวิธี XML PATH จะระเบิดขึ้นหากคุณมีอิโมจิหรืออักขระพิเศษ / ตัวแทนในข้อมูลของคุณ !!!
devinbost

1
รหัสนี้ส่งผลให้ข้อความที่เข้ารหัส xml ( &เปลี่ยนเป็น&และอื่น ๆ ) ถูกต้องมากขึ้นfor xmlวิธีการแก้ปัญหาที่มีให้ที่นี่
Frédéric

36

สำหรับพวกเราที่พบสิ่งนี้ และไม่ได้ใช้ฐานข้อมูล Azure SQL:

STRING_AGG()ใน PostgreSQL, SQL Server 2017 และ Azure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.microsoft.com/en-us/sql/t-sql/ ฟังก์ชัน / string-agg-transact-sql

GROUP_CONCAT()ใน MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat

(ขอบคุณ @Brianjorden และ @milanio สำหรับการอัปเดต Azure)

ตัวอย่างรหัส:

select Id
, STRING_AGG(Name, ', ') Names 
from Demo
group by Id

SQL Fiddle: http://sqlfiddle.com/#!18/89251/1


1
ฉันเพิ่งทดสอบและตอนนี้ใช้งานได้ดีกับ Azure SQL Database
milanio

5
STRING_AGGได้รับการผลักดันกลับไปในปี 2017 ซึ่งไม่สามารถใช้ได้ในปี 2559
Morgan Thrapp

1
ขอบคุณ Aamir และ Morgan Thrapp สำหรับการเปลี่ยนแปลงเวอร์ชันของเซิร์ฟเวอร์ SQL อัปเดตแล้ว (ในขณะที่เขียนมีการอ้างว่ารองรับในเวอร์ชัน 2016)
Hrobky

26

แม้ว่าคำตอบ @serge จะถูกต้อง แต่ฉันเปรียบเทียบการใช้เวลาของเขากับ xmlpath และฉันพบว่า xmlpath นั้นเร็วกว่ามาก ฉันจะเขียนโค้ดเปรียบเทียบและคุณสามารถตรวจสอบได้ด้วยตัวเอง นี่คือวิธี @serge:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (ID int, Name nvarchar(50))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE()

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM @YourTable
),
Concatenated AS
(
    SELECT ID, CAST(Name AS nvarchar) AS FullName, Name, NameNumber, NameCount FROM Partitioned WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, CAST(C.FullName + ', ' + P.Name AS nvarchar), P.Name, P.NameNumber, P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C ON P.ID = C.ID AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 54 milliseconds

และนี่คือวิธี xmlpath:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (@counter, ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE();

set nocount off
SELECT
    t1.HeaderValue
        ,STUFF(
                   (SELECT
                        ', ' + t2.ChildValue
                        FROM @YourTable t2
                        WHERE t1.HeaderValue=t2.HeaderValue
                        ORDER BY t2.ChildValue
                        FOR XML PATH(''), TYPE
                   ).value('.','varchar(max)')
                   ,1,2, ''
              ) AS ChildValues
    FROM @YourTable t1
    GROUP BY t1.HeaderValue

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 4 milliseconds

2
+1 คุณ QMaster (แห่งศาสตร์มืด) คุณ! ฉันมีความแตกต่างที่น่าทึ่งมากขึ้น (~ 3000 msec CTE เทียบกับ ~ 70 msec XML บน SQL Server 2008 R2 บน Windows Server 2008 R2 บน Intel Xeon E5-2630 v4 @ 2.20 GHZ x2 w / ~ 1 GB ฟรี) เพียงข้อเสนอแนะคือ 1) การใช้งานทั้ง OP หรือ (ยิ่ง) เงื่อนไขทั่วไปสำหรับทั้งสองรุ่น 2) ตั้งแต่ OP ของ Q. เป็นวิธีการ "concatenate / รวมสตริง " และนี่เป็นสิ่งจำเป็นสำหรับสตริง (เทียบกับตัวเลขค่า), ทั่วไป คำศัพท์ทั่วไปเกินไป เพียงใช้ "GroupNumber" และ "StringValue", 3) ประกาศและใช้ตัวแปร "Delimiter" และใช้ "Len (Delimiter)" เทียบกับ "2"
ทอม

1
+1 สำหรับการไม่ขยายอักขระพิเศษเป็นการเข้ารหัส XML (เช่น '&' ไม่ได้รับการขยายเป็น '& amp;' เหมือนในโซลูชันที่ด้อยกว่าอื่น ๆ อีกมากมาย)
Reversed Engineer

16

ปรับปรุง: Ms SQL Server 2017+ ฐานข้อมูล Azure SQL

คุณสามารถใช้: STRING_AGG.

การใช้งานค่อนข้างง่ายสำหรับคำขอของ OP:

SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id

อ่านเพิ่มเติม

คำตอบที่ไม่ใช่คำตอบเก่าของฉันถูกลบอย่างถูกต้อง (ซ้ายในชั้นเชิงด้านล่าง) แต่ถ้าใครเกิดขึ้นที่นี่ในอนาคตมีข่าวดี พวกเขาได้แสดงถึง STRING_AGG () ใน Azure SQL Database ด้วยเช่นกัน สิ่งนี้ควรให้ฟังก์ชันการทำงานที่ถูกต้องตามที่ขอไว้ในโพสต์นี้พร้อมกับเนทีฟและการสนับสนุน @hrobky ได้กล่าวถึงสิ่งนี้ก่อนหน้านี้ว่าเป็นคุณลักษณะของ SQL Server 2016 ในเวลานั้น

- โพสต์เก่า: มีชื่อเสียงไม่เพียงพอที่จะตอบกลับ @hrobky โดยตรง แต่ STRING_AGG ดูดีมากอย่างไรก็ตามขณะนี้มีให้บริการใน SQL Server 2016 vNext เท่านั้น หวังว่าจะเป็นไปตาม Azure SQL Datababse เร็ว ๆ นี้เช่นกัน ..


2
ฉันเพิ่งทดสอบและใช้งานได้เหมือนมีเสน่ห์ใน Azure SQL Database
milanio

4
STRING_AGG()ถูกระบุว่าพร้อมใช้งานใน SQL Server 2017 ในระดับความเข้ากันได้ใด ๆ docs.microsoft.com/en-us/sql/t-sql/functions/…
ผู้ใช้

1
ใช่. STRING_AGG ไม่มีใน SQL Server 2016
Magne

2

ฉันพบว่าคำตอบของ Serge นั้นมีแนวโน้มมาก แต่ฉันก็พบปัญหาด้านประสิทธิภาพด้วยเช่นกัน อย่างไรก็ตามเมื่อฉันปรับโครงสร้างใหม่ให้ใช้ตารางชั่วคราวและไม่รวมตาราง CTE สองตารางประสิทธิภาพจะเปลี่ยนจาก 1 นาที 40 วินาทีเป็นวินาทีย่อยสำหรับ 1,000 ระเบียนที่รวมกัน สำหรับทุกคนที่ต้องการทำสิ่งนี้โดยไม่ต้องใช้ FOR XML บน SQL Server เวอร์ชันเก่า:

DECLARE @STRUCTURED_VALUES TABLE (
     ID                 INT
    ,VALUE              VARCHAR(MAX) NULL
    ,VALUENUMBER        BIGINT
    ,VALUECOUNT         INT
);

INSERT INTO @STRUCTURED_VALUES
SELECT   ID
        ,VALUE
        ,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY VALUE) AS VALUENUMBER
        ,COUNT(*) OVER (PARTITION BY ID)    AS VALUECOUNT
FROM    RAW_VALUES_TABLE;

WITH CTE AS (
    SELECT   SV.ID
            ,SV.VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    WHERE   VALUENUMBER = 1

    UNION ALL

    SELECT   SV.ID
            ,CTE.VALUE + ' ' + SV.VALUE AS VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    JOIN    CTE 
        ON  SV.ID = CTE.ID
        AND SV.VALUENUMBER = CTE.VALUENUMBER + 1

)
SELECT   ID
        ,VALUE
FROM    CTE
WHERE   VALUENUMBER = VALUECOUNT
ORDER BY ID
;

1

คุณสามารถใช้ + = เพื่อเชื่อมสตริงเข้าด้วยกันตัวอย่างเช่น:

declare @test nvarchar(max)
set @test = ''
select @test += name from names

หากคุณเลือก @test มันจะทำให้คุณมีชื่อทั้งหมดที่เรียงต่อกัน


โปรดระบุภาษาหรือเวอร์ชันของ SQL ตั้งแต่เมื่อใดที่รองรับ
Hrobky

ใช้งานได้ใน SQL Server 2012 โปรดทราบว่ารายการที่คั่นด้วยจุลภาคสามารถสร้างได้ด้วยselect @test += name + ', ' from names
Art Schmidt

4
สิ่งนี้ใช้พฤติกรรมที่ไม่ได้กำหนดและไม่ปลอดภัย โดยเฉพาะอย่างยิ่งมีแนวโน้มที่จะให้ผลลัพธ์ที่แปลก / ไม่ถูกต้องหากคุณมีORDER BYข้อความค้นหา คุณควรใช้หนึ่งในทางเลือกอื่นที่ระบุไว้
Dannnno

1
แบบสอบถามประเภทนี้ไม่เคยกำหนดพฤติกรรมและใน SQL Server 2019 เราพบว่ามีลักษณะการทำงานที่ไม่ถูกต้องสม่ำเสมอกว่าในเวอร์ชันก่อนหน้า อย่าใช้แนวทางนี้
Matthew Rodatus
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.