Cách tối ưu để nối / gộp các chuỗi


102

Tôi đang tìm cách tổng hợp các chuỗi từ các hàng khác nhau thành một hàng duy nhất. Tôi đang tìm cách thực hiện việc này ở nhiều nơi khác nhau, vì vậy có một chức năng hỗ trợ việc này sẽ rất tốt. Tôi đã thử các giải pháp bằng cách sử dụng COALESCEFOR XML, nhưng họ không cắt nó cho tôi.

Tổng hợp chuỗi sẽ làm một cái gì đó như sau:

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

Tôi đã xem xét các hàm tổng hợp do CLR định nghĩa để thay thế COALESCEFOR XML, nhưng rõ ràng SQL Azure không hỗ trợ nội dung do CLR định nghĩa, đó là một điều khó khăn đối với tôi vì tôi biết có thể sử dụng nó sẽ giải quyết được rất nhiều vấn đề cho tôi.

Có workaround có thể, hoặc các phương pháp tối ưu tương tự (mà có thể không được như tối ưu như CLR, nhưng hey Tôi sẽ đưa những gì tôi có thể nhận được) mà tôi có thể sử dụng để tổng hợp công cụ của tôi?


Theo cách nào for xmlkhông làm việc cho bạn?
Mikael Eriksson

4
Nó hoạt động, nhưng tôi đã xem xét kế hoạch thực thi và mỗi kế hoạch for xmlcho thấy mức sử dụng 25% về hiệu suất truy vấn (phần lớn truy vấn!)
matt

2
Có nhiều cách khác nhau để thực hiện for xml pathtruy vấn. Một số nhanh hơn những người khác. Nó có thể phụ thuộc vào dữ liệu của bạn nhưng những thứ distinctđang sử dụng theo kinh nghiệm của tôi chậm hơn so với sử dụng group by. Và nếu bạn đang sử dụng .value('.', nvarchar(max))để có được các giá trị nối bạn nên thay đổi điều đó để.value('./text()[1]', nvarchar(max))
Mikael Eriksson

3
Câu trả lời được chấp nhận của bạn giống tôi câu trả lời trên stackoverflow.com/questions/11137075/... mà tôi nghĩ là nhanh hơn so với XML. Đừng để bị lừa bởi chi phí truy vấn, bạn cần nhiều dữ liệu để xem cái nào nhanh hơn. XML nhanh hơn, đây là câu trả lời của @ MikaelEriksson cho cùng một câu hỏi . Chọn phương pháp tiếp cận XML
Michael Buen

2
Vui lòng bỏ phiếu cho giải pháp gốc cho giải pháp này tại đây: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Câu trả lời:


67

GIẢI PHÁP

Định nghĩa về tối ưu có thể khác nhau, nhưng đây là cách nối các chuỗi từ các hàng khác nhau bằng Transact SQL thông thường, sẽ hoạt động tốt trong 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

GIẢI TRÌNH

Cách tiếp cận này bao gồm ba bước:

  1. Đánh số các hàng bằng cách sử dụng OVERPARTITIONnhóm và sắp xếp chúng khi cần thiết cho việc nối. Kết quả là PartitionedCTE. Chúng tôi giữ số lượng hàng trong mỗi phân vùng để lọc kết quả sau này.

  2. Sử dụng CTE đệ quy ( Concatenated) lặp qua số hàng ( NameNumbercột) thêm Namegiá trị vào FullNamecột.

  3. Lọc ra tất cả các kết quả trừ những kết quả có kết quả cao nhất NameNumber.

Xin lưu ý rằng để làm cho truy vấn này có thể dự đoán được, người ta phải xác định cả nhóm (ví dụ: trong kịch bản của bạn các hàng với cùng một IDđược nối) và sắp xếp (tôi giả sử rằng bạn chỉ cần sắp xếp chuỗi theo thứ tự bảng chữ cái trước khi nối).

Tôi đã nhanh chóng thử nghiệm giải pháp trên SQL Server 2012 với dữ liệu sau:

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

Kết quả truy vấn:

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

5
Tôi đã kiểm tra mức tiêu thụ thời gian theo cách này so với xmlpath và tôi đạt khoảng 4 mili giây so với khoảng 54 mili giây. vì vậy cách xmplath tốt hơn đặc biệt trong các trường hợp lớn. Tôi sẽ viết mã so sánh trong một câu trả lời riêng.
QMaster

Nó tốt hơn nhiều vì cách tiếp cận này chỉ hoạt động với tối đa 100 giá trị.
Romano Zumbé

@ romano-zumbé Sử dụng MAXRECURSION để đặt giới hạn CTE thành bất kỳ thứ gì bạn cần.
Serge Belov

1
Đáng ngạc nhiên, CTE chậm hơn đối với tôi. sqlperformance.com/2014/08/t-sql-queries/… so sánh một loạt các kỹ thuật và dường như đồng ý với kết quả của tôi.
Nickolay

Giải pháp này cho bảng có hơn 1 triệu bản ghi không hoạt động. Ngoài ra, chúng tôi có một giới hạn về độ sâu đệ quy
Ardalan Shahgholi

51

Các phương pháp sử dụng FOR XML PATH như bên dưới có thực sự chậm không? Itzik Ben-Gan viết rằng phương pháp này có hiệu suất tốt trong cuốn sách Truy vấn T-SQL của mình (Theo quan điểm của tôi, ông Ben-Gan là một nguồn đáng tin cậy).

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

Đừng quên đặt một chỉ mục trên idcột đó khi kích thước của bảng trở thành vấn đề.
milivojeviCH

1
Và sau khi đọc như thế nào stuff / cho công việc con đường xml ( stackoverflow.com/a/31212160/1026 ), tôi tự tin rằng đó là một giải pháp tốt mặc dù XML trong tên của nó :)
Nickolay

1
@slackterman Phụ thuộc vào số lượng bản ghi được xử lý. Tôi nghĩ rằng XML bị thiếu ở số lượng thấp, so với CTE, nhưng ở số lượng khối lượng cao hơn, làm giảm bớt giới hạn của Recursion Dept và dễ điều hướng hơn, nếu được thực hiện một cách chính xác và ngắn gọn.
GoldBishop

Đối với các phương thức PATH XML sẽ nổ tung nếu bạn có biểu tượng cảm xúc hoặc các ký tự đặc biệt / thay thế trong dữ liệu của mình !!!
devinbost

1
Mã này dẫn đến văn bản được mã hóa xml (được &chuyển sang &, v.v.). Một for xmlgiải pháp đúng hơn được cung cấp ở đây .
Frédéric

33

Đối với những người trong chúng tôi, những người đã tìm thấy điều này và không sử dụng Cơ sở dữ liệu Azure SQL:

STRING_AGG()trong PostgreSQL, SQL Server 2017 và Azure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.microsoft.com/en-us/sql/t-sql/ functions / string-agg-transact-sql

GROUP_CONCAT()trong MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html# Chức năng_group-concat

(Cảm ơn @Brianjorden và @milanio về bản cập nhật Azure)

Mã ví dụ:

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

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


1
Tôi vừa thử nghiệm nó và bây giờ nó hoạt động tốt với Cơ sở dữ liệu Azure SQL.
milanio

5
STRING_AGGđã bị đẩy trở lại năm 2017. Nó không có sẵn vào năm 2016.
Morgan Thrapp

1
Cảm ơn bạn, Aamir và Morgan Thrapp đã thay đổi phiên bản SQL Server. Đã cập nhật. (Tại thời điểm viết bài, nó đã được tuyên bố là được hỗ trợ trong phiên bản 2016.)
Hrobky

25

Mặc dù câu trả lời @serge là đúng nhưng tôi đã so sánh mức tiêu thụ thời gian của anh ấy với xmlpath và tôi thấy xmlpath nhanh hơn. Tôi sẽ viết mã so sánh và bạn có thể tự mình kiểm tra. Đây là cách @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

Và đây là cách 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, bạn QMaster (của Dark Arts) bạn! Tôi có một sự khác biệt ấn tượng hơn. (~ 3000 msec CTE so với ~ 70 msec XML trên SQL Server 2008 R2 trên Windows Server 2008 R2 trên Intel Xeon E5-2630 v4 @ 2,20 GHZ x2 w / ~ 1 GB trống). Chỉ có các đề xuất là: 1) Sử dụng thuật ngữ chung của OP hoặc (tốt hơn là) cho cả hai phiên bản, 2) Vì Q. của OP là cách "nối / tổng hợp các chuỗi " và điều này chỉ cần thiết cho các chuỗi (so với một giá trị số ), chung chung thuật ngữ quá chung chung. Chỉ cần sử dụng "GroupNumber" và "StringValue", 3) Khai báo và sử dụng Biến "Dấu phân cách" và sử dụng "Len (Dấu phân cách)" so với "2".
Tom

1
+1 vì không mở rộng ký tự đặc biệt sang mã hóa XML (ví dụ: '&' không được mở rộng thành '& amp;' giống như trong rất nhiều giải pháp kém chất lượng khác)
Reversed Engineer

13

Cập nhật: Ms SQL Server 2017+, Cơ sở dữ liệu Azure SQL

Bạn có thể sử dụng: STRING_AGG.

Cách sử dụng khá đơn giản đối với yêu cầu của OP:

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

Đọc thêm

Chà, câu trả lời cũ của tôi đã bị xóa đúng (còn nguyên bên dưới), nhưng nếu có ai đó tình cờ hạ cánh ở đây trong tương lai, thì có một tin tốt. Họ cũng đã chèn STRING_AGG () trong Cơ sở dữ liệu Azure SQL. Điều đó sẽ cung cấp chức năng chính xác được yêu cầu ban đầu trong bài đăng này với hỗ trợ gốc và được tích hợp sẵn. @hrobky đã đề cập điều này trước đây như một tính năng của SQL Server 2016 vào thời điểm đó.

--- Bài cũ: Ở đây không đủ uy tín để trả lời trực tiếp @hrobky, nhưng STRING_AGG trông rất tuyệt, tuy nhiên hiện tại nó chỉ khả dụng trong SQL Server 2016 vNext. Hy vọng rằng nó cũng sẽ sớm được chuyển sang Azure SQL Datababse ..


2
Tôi vừa mới thử nghiệm nó và nó hoạt động như một nét duyên dáng trong cơ sở dữ liệu SQL Azure
milanio

4
STRING_AGG()được tuyên bố là có sẵn trong SQL Server 2017, ở bất kỳ mức độ tương thích nào. docs.microsoft.com/en-us/sql/t-sql/functions/…
a CVn

1
Đúng. STRING_AGG không khả dụng trong SQL Server 2016.
Magne

2

Bạn có thể sử dụng + = để nối các chuỗi, ví dụ:

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

nếu bạn chọn @test, nó sẽ cung cấp cho bạn tất cả các tên được nối


Vui lòng chỉ định phương ngữ hoặc phiên bản SQL kể từ khi nó được hỗ trợ.
Hrobky

Này hoạt động trong SQL Server 2012. Lưu ý rằng một danh sách bằng dấu phẩy có thể được tạo ra vớiselect @test += name + ', ' from names
Nghệ thuật Schmidt

4
Điều này sử dụng hành vi không xác định và không an toàn. Điều này đặc biệt có thể đưa ra một kết quả lạ / không chính xác nếu bạn có một ORDER BYtruy vấn trong truy vấn của mình. Bạn nên sử dụng một trong những lựa chọn thay thế được liệt kê.
Dannnno

1
Loại truy vấn này chưa bao giờ được xác định hành vi và trong SQL Server 2019, chúng tôi nhận thấy nó có hành vi không chính xác nhất quán hơn so với các phiên bản trước. Đừng sử dụng cách tiếp cận này.
Matthew Rodatus

2

Tôi thấy câu trả lời của Serge rất hứa hẹn, nhưng tôi cũng gặp phải các vấn đề về hiệu suất với nó như đã viết. Tuy nhiên, khi tôi cấu trúc lại nó để sử dụng các bảng tạm thời và không bao gồm các bảng CTE kép, hiệu suất đã giảm từ 1 phút 40 giây xuống dưới giây cho 1000 bản ghi kết hợp. Đây là phần mềm dành cho bất kỳ ai cần thực hiện việc này mà không có FOR XML trên các phiên bản SQL Server cũ hơn:

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