Cara optimal untuk menggabungkan / menggabungkan string

102

Saya menemukan cara untuk menggabungkan string dari baris yang berbeda menjadi satu baris. Saya ingin melakukan ini di banyak tempat berbeda, jadi memiliki fungsi untuk memfasilitasi ini akan menyenangkan. Saya sudah mencoba solusi menggunakan COALESCEdan FOR XML, tetapi solusi tersebut tidak cocok untuk saya.

Agregasi string akan melakukan sesuatu seperti ini:

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

Saya telah melihat fungsi agregat yang ditentukan CLR sebagai pengganti COALESCEdan FOR XML, tetapi tampaknya SQL Azure tidak mendukung hal-hal yang ditentukan CLR, yang menyebalkan bagi saya karena saya tahu bisa menggunakannya akan menyelesaikan banyak hal. masalah bagi saya.

Apakah ada solusi yang mungkin, atau metode optimal serupa (yang mungkin tidak seoptimal CLR, tapi hei saya akan mengambil apa yang bisa saya dapatkan) yang dapat saya gunakan untuk mengumpulkan barang-barang saya?

Matt
sumber
Dengan cara apa yang for xmltidak berhasil untuk Anda?
Mikael Eriksson
4
Itu berhasil, tetapi saya melihat pada rencana eksekusi dan masing-masing for xmlmenunjukkan penggunaan 25% dalam hal kinerja kueri (sebagian besar kueri!)
matt
2
Ada berbagai cara untuk melakukan for xml pathkueri. Beberapa lebih cepat dari yang lain. Itu bisa tergantung pada data Anda tetapi yang menggunakan distinctmenurut pengalaman saya lebih lambat daripada menggunakan group by. Dan jika Anda menggunakan .value('.', nvarchar(max))untuk mendapatkan nilai gabungan Anda harus mengubahnya menjadi.value('./text()[1]', nvarchar(max))
Mikael Eriksson
3
Jawaban yang diterima Anda menyerupai saya jawaban atas stackoverflow.com/questions/11137075/... yang saya pikir lebih cepat dari XML. Jangan tertipu oleh biaya kueri, Anda memerlukan banyak data untuk melihat mana yang lebih cepat. XML lebih cepat, yang terjadi menjadi @ MikaelEriksson ini jawabannya pada yang sama pertanyaan . Pilih pendekatan XML
Michael Buen
2
Silakan pilih solusi asli untuk ini di sini: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Jawaban:

67

LARUTAN

Definisi optimal dapat bervariasi, tetapi berikut ini cara menggabungkan string dari baris yang berbeda menggunakan Transact SQL biasa, yang akan berfungsi dengan baik di 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

PENJELASAN

Pendekatan ini terdiri dari tiga langkah:

  1. Beri nomor baris yang menggunakan OVERdan PARTITIONmengelompokkan dan mengurutkannya sesuai kebutuhan untuk penggabungan. Hasilnya adalah PartitionedCTE. Kami menyimpan jumlah baris di setiap partisi untuk menyaring hasilnya nanti.

  2. Menggunakan rekursif CTE ( Concatenated) iterasi melalui nomor baris ( NameNumberkolom) menambahkan Namenilai ke FullNamekolom.

  3. Filter semua hasil kecuali yang tertinggi NameNumber.

Harap diingat bahwa untuk membuat kueri ini dapat diprediksi, seseorang harus menentukan pengelompokan (misalnya, dalam baris skenario Anda dengan baris yang sama IDdigabung) dan pengurutan (saya berasumsi bahwa Anda cukup mengurutkan string menurut abjad sebelum penggabungan).

Saya telah menguji solusi dengan cepat di SQL Server 2012 dengan data berikut:

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

Hasil query:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
Serge Belov
sumber
5
Saya memeriksa konsumsi waktu dengan cara ini terhadap xmlpath dan saya mencapai sekitar 4 milidetik vs sekitar 54 milidetik. jadi cara xmplath lebih baik khususnya dalam kasus besar. Saya akan menulis kode pembanding dalam jawaban terpisah.
QMaster
Jauh lebih baik karena pendekatan ini hanya bekerja untuk 100 nilai maksimum.
Romano Zumbé
@ romano-zumbé Gunakan MAXRECURSION untuk menyetel batas CTE sesuai kebutuhan Anda.
Serge Belov
1
Anehnya, CTE jauh lebih lambat bagi saya. sqlperformance.com/2014/08/t-sql-queries/… membandingkan banyak teknik, dan tampaknya setuju dengan hasil saya.
Nickolay
Solusi untuk tabel dengan lebih dari 1 juta catatan ini tidak berfungsi. Juga, kami memiliki batas kedalaman rekursif
Ardalan Shahgholi
51

Apakah metode yang menggunakan FOR XML PATH seperti di bawah ini benar-benar lambat? Itzik Ben-Gan menulis bahwa metode ini memiliki kinerja yang baik dalam bukunya T-SQL Querying (Mr. Ben-Gan adalah sumber yang dapat dipercaya, menurut saya).

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
pembuat slachter
sumber
Jangan lupa untuk memberi indeks pada idkolom itu setelah ukuran tabel menjadi masalah.
milivojeviCH
1
Dan setelah membaca cara kerja stuff / for xml path ( stackoverflow.com/a/31212160/1026 ), saya yakin ini adalah solusi yang bagus meskipun namanya XML :)
Nickolay
1
@slackterman Tergantung pada jumlah rekaman yang akan dioperasikan. Saya pikir XML kurang pada jumlah yang rendah, dibandingkan dengan CTE, tetapi pada jumlah volume atas, mengurangi batasan Departemen Rekursi dan lebih mudah dinavigasi, jika dilakukan dengan benar dan ringkas.
GoldBishop
UNTUK metode XML PATH meledak jika Anda memiliki emoji atau karakter khusus / pengganti dalam data Anda !!!
devinbost
1
Kode ini menghasilkan teks berenkode xml ( &dialihkan ke &, dan seterusnya). Solusi yang lebih tepat for xmldisediakan di sini .
Frédéric
33

Bagi kita yang menemukan ini dan tidak menggunakan Database Azure SQL:

STRING_AGG()di PostgreSQL, SQL Server 2017 dan 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()di MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat

(Terima kasih kepada @Brianjorden dan @milanio untuk pembaruan Azure)

Kode Contoh:

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

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

Hrobky
sumber
1
Saya baru saja mengujinya dan sekarang berfungsi dengan baik dengan Azure SQL Database.
milanio
5
STRING_AGGdidorong kembali ke 2017. Ini tidak tersedia pada 2016.
Morgan Thrapp
1
Terima kasih, Aamir dan Morgan Thrapp atas perubahan versi SQL Server. Diperbarui. (Pada saat penulisan, ini diklaim didukung dalam versi 2016.)
Hrobky
25

Meskipun jawaban @serge benar tetapi saya membandingkan konsumsi waktu jalannya terhadap xmlpath dan saya menemukan xmlpath sangat cepat. Saya akan menulis kode pembanding dan Anda dapat memeriksanya sendiri. Ini adalah cara @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

Dan ini adalah cara 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
QMaster
sumber
2
+1, Anda QMaster (Ilmu Hitam) Anda! Saya mendapat perbedaan yang lebih dramatis. (~ 3000 msec CTE vs. ~ 70 msec XML pada SQL Server 2008 R2 pada Windows Server 2008 R2 pada Intel Xeon E5-2630 v4 @ 2,20 GHZ x2 w / ~ 1 GB gratis). Hanya sarannya adalah: 1) Gunakan OP atau (lebih disukai) istilah umum untuk kedua versi, 2) Karena OP Q. adalah bagaimana "menggabungkan / menggabungkan string " dan ini hanya diperlukan untuk string (vs. nilai numerik ), generik istilah terlalu umum. Cukup gunakan "GroupNumber" dan "StringValue", 3) Deklarasikan dan gunakan Variabel "Pembatas" dan gunakan "Len (Pembatas)" vs. "2".
Tom
1
+1 untuk tidak memperluas karakter khusus ke pengkodean XML (misalnya '&' tidak diperluas menjadi '& amp;' seperti di banyak solusi inferior lainnya)
Reversed Engineer
13

Pembaruan: Ms SQL Server 2017+, Azure SQL Database

Kamu bisa memakai: STRING_AGG .

Penggunaannya cukup sederhana untuk permintaan OP:

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

Baca lebih banyak

Nah jawaban lama saya dihapus dengan benar (kiri dalam kebijaksanaan di bawah), tetapi jika ada yang kebetulan mendarat di sini di masa depan, ada kabar baik. Mereka telah menerapkan STRING_AGG () di Azure SQL Database juga. Itu harus menyediakan fungsionalitas tepat yang awalnya diminta dalam posting ini dengan dukungan bawaan dan bawaan. @hrobky menyebutkan ini sebelumnya sebagai fitur SQL Server 2016 pada saat itu.

--- Posting Lama: Reputasi di sini tidak cukup untuk membalas @hrobky secara langsung, tetapi STRING_AGG tampak hebat, namun saat ini hanya tersedia di SQL Server 2016 vNext. Semoga ini juga akan segera menyusul ke Azure SQL Databse ..

Brian Jorden
sumber
2
Saya baru saja mengujinya dan berfungsi seperti pesona di Azure SQL Database
milanio
4
STRING_AGG()dinyatakan tersedia di SQL Server 2017, di tingkat kompatibilitas apa pun. docs.microsoft.com/en-us/sql/t-sql/functions/…
CVn
1
Iya. STRING_AGG tidak tersedia di SQL Server 2016.
Magne
2

Anda dapat menggunakan + = untuk menggabungkan string, misalnya:

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

jika Anda memilih @test, ini akan memberi Anda semua nama yang digabungkan

jvc
sumber
Harap tentukan dialek atau versi SQL sejak kapan itu didukung.
Hrobky
Ini berfungsi di SQL Server 2012. Perhatikan bahwa daftar yang dipisahkan koma dapat dibuat denganselect @test += name + ', ' from names
Art Schmidt
4
Ini menggunakan perilaku yang tidak ditentukan, dan tidak aman. Ini kemungkinan besar akan memberikan hasil yang aneh / salah jika Anda memiliki ORDER BYpertanyaan dalam kueri Anda. Anda harus menggunakan salah satu alternatif yang terdaftar.
Dannnno
1
Jenis kueri ini tidak pernah ditentukan perilaku, dan di SQL Server 2019 kami menemukan perilaku yang salah lebih konsisten daripada di versi sebelumnya. Jangan gunakan pendekatan ini.
Matthew Rodatus
2

Saya menemukan jawaban Serge sangat menjanjikan, tetapi saya juga mengalami masalah kinerja dengan itu seperti yang tertulis. Namun, ketika saya menata ulangnya untuk menggunakan tabel sementara dan tidak menyertakan tabel CTE ganda, kinerja berubah dari 1 menit 40 detik menjadi sub-detik untuk 1000 rekaman gabungan. Ini untuk siapa saja yang perlu melakukan ini tanpa FOR XML di versi SQL Server yang lebih lama:

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
;
Tom Halladay
sumber