Dizeleri birleştirmenin / toplamanın en uygun yolu


106

Farklı satırlardan dizeleri tek bir satırda toplamanın bir yolunu buluyorum. Bunu birçok farklı yerde yapmak istiyorum, bu yüzden bunu kolaylaştıracak bir işleve sahip olmak güzel olurdu. Ben kullanarak çözümler denedim COALESCEve FOR XMLfakat sadece benim için kesmeyin.

Dize toplama şunun gibi bir şey yapacaktır:

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

CLR tanımlı toplama işlevlerine bir alternatif olarak baktım COALESCEve FOR XMLgörünüşe göre SQL Azure , CLR tanımlı şeyleri desteklemiyor, bu benim için bir acı, çünkü onu kullanmanın birçok şeyi çözeceğini biliyorum. benim için sorunlar.

Olası geçici çözüm, ya da (CLR olduğunca en iyi olmayabilir ama benzer optimum yöntem var mı hey ben eşyalarımı toplamak için kullanabileceği ben ne alabilirim edeceğiz)?


for xmlSizin için ne şekilde çalışmıyor ?
Mikael Eriksson

4
İşe yarıyor, ancak yürütme planına bir göz attım ve her for xmlbiri sorgu performansı açısından% 25 kullanım gösteriyor (sorgunun büyük bir kısmı!)
matt

2
for xml pathSorguyu yapmanın farklı yolları vardır . Bazıları diğerlerinden daha hızlı. Verilerinize bağlı olabilir, ancak kullananlar distinctbenim deneyimime göre kullanmaktan daha yavaştır group by. Ve .value('.', nvarchar(max))birleştirilmiş değerleri elde etmek için kullanıyorsanız , bunu şu şekilde değiştirmelisiniz.value('./text()[1]', nvarchar(max))
Mikael Eriksson

3
Sizin kabul cevap benim andıran cevap üzerine stackoverflow.com/questions/11137075/... daha hızlı XML daha düşündük. Sorgu maliyetine aldanmayın, hangisinin daha hızlı olduğunu görmek için bol miktarda veriye ihtiyacınız var. XML daha hızlıdır, bu da @ MikaelEriksson'un aynı soruya cevabıdır . XML yaklaşımını seçin
Michael Buen

2
Lütfen bunun için yerel bir çözüm için oy verin: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Yanıtlar:


69

ÇÖZÜM

Optimal tanımı değişebilir, ancak Azure'da sorunsuz çalışması gereken normal Transact SQL kullanarak farklı satırlardan dizeleri nasıl birleştireceğiniz burada açıklanmaktadır.

;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

AÇIKLAMA

Yaklaşım üç adıma indirgeniyor:

  1. Birleştirme için gerektiği gibi kullanarak OVERve PARTITIONgruplayarak ve sıralayarak satırları numaralandırın . Sonuç PartitionedCTE'dir. Sonuçları daha sonra filtrelemek için her bölümdeki satır sayısını tutuyoruz.

  2. Yinelemeli CTE ( Concatenated) kullanarak, NameNumbersütuna Namedeğerler ekleyerek satır numaralarını ( sütun) yineleyin FullName.

  3. En yüksek olanlar dışındaki tüm sonuçları filtreleyin NameNumber.

Lütfen bu sorguyu öngörülebilir kılmak için, hem gruplamayı (örneğin, senaryo satırlarınızda aynı IDolan satırlar birleştirilir) hem de sıralamayı (dizeyi bitiştirmeden önce alfabetik olarak sıraladığınızı varsaydım) tanımlamanız gerektiğini unutmayın.

Çözümü aşağıdaki verilerle SQL Server 2012'de hızlıca test ettim:

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

Sorgu sonucu:

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

5
Bu şekilde zaman tüketimini xmlpath'e karşı kontrol ettim ve yaklaşık 4 milisaniye yerine yaklaşık 54 milisaniyeye ulaştım. bu nedenle xmplath yolu özellikle büyük durumlarda daha iyidir. Karşılaştırma kodunu ayrı bir cevapla yazacağım.
QMaster

Bu yaklaşım yalnızca maksimum 100 değer için çalıştığı için çok daha iyidir.
Romano Zumbé

@ romano-zumbé CTE sınırını ihtiyacınız olana ayarlamak için MAXRECURSION kullanın.
Serge Belov

1
Şaşırtıcı bir şekilde, CTE benim için çok daha yavaştı. sqlperformance.com/2014/08/t-sql-queries/… bir dizi tekniği karşılaştırıyor ve sonuçlarımla aynı fikirde görünüyor.
Nickolay

1 milyondan fazla kaydı olan bir tablo için bu çözüm işe yaramıyor. Ayrıca yinelemeli derinlikte bir sınırımız var
Ardalan Shahgholi

52

Aşağıdaki gibi FOR XML PATH kullanan yöntemler gerçekten çok mu yavaş? Itzik Ben-Gan, bu yöntemin T-SQL Sorgulama kitabında iyi bir performansa sahip olduğunu yazıyor (Bay Ben-Gan bana göre güvenilir bir kaynaktır).

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

idBir tablonun boyutu bir sorun haline geldiğinde o sütuna bir dizin koymayı unutmayın .
milivojeviCH

2
Ve xml yolunun nasıl çalıştığını okuduktan sonra ( stackoverflow.com/a/31212160/1026 ),
ismindeki

1
@slackterman Çalıştırılacak kayıt sayısına bağlıdır. XML'in CTE'ye kıyasla düşük sayılarda yetersiz olduğunu düşünüyorum, ancak yüksek hacim sayılarında Özyineleme Derinliği sınırlamasını hafifletiyor ve doğru ve kısa bir şekilde yapılırsa gezinmesi daha kolay.
GoldBishop

Verilerinizde emojiler veya özel / yedek karakterler varsa, XML YOLU İÇİN yöntemler patlar !!!
devinbost

1
Bu kod, xml kodlu metinle sonuçlanır ( &değiştirilir &, vb.). Daha doğru for xmlbir çözüm sağlanır burada .
Frédéric

36

Bunu bulan bizler için ve Azure SQL Veritabanı kullanmıyor:

STRING_AGG()PostgreSQL, SQL Server 2017 ve Azure SQL'de
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.microsoft.com/en-us/sql/t-sql/ işlevler / string-agg-transact-sql

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

(Azure güncellemesi için @ Brianjorden ve @milanio'ya teşekkürler)

Örnek Kod:

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

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


1
Henüz test ettim ve şimdi Azure SQL Veritabanı ile sorunsuz çalışıyor.
milanio

5
STRING_AGG2017'ye geri gönderildi. 2016'da mevcut değil.
Morgan Thrapp

1
Aamir ve Morgan Thrapp, SQL Server sürüm değişikliği için teşekkür ederiz. Güncellenmiş. (Yazma sırasında 2016 sürümünde desteklendiği iddia edilmişti.)
Hrobky

26

@Serge cevabı doğru olmasına rağmen, zaman tüketimini xmlpath ile karşılaştırdım ve xmlpath'in çok daha hızlı olduğunu buldum. Karşılaştırma kodunu yazacağım ve kendiniz kontrol edebilirsiniz. Bu, @serge yöntemidir:

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

Ve bu xmlpath yoludur:

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, sen QMaster (Karanlık Sanatların) seni! Daha da dramatik bir farkım var. (Intel Xeon E5-2630 v4 @ 2.20 GHZ x2 w / ~ 1 GB ücretsiz Windows Server 2008 R2 üzerinde ~ 3000 msn CTE ile SQL Server 2008 R2 üzerinde ~ 70 msn XML). Yalnızca öneriler şunlardır: 1) Her iki sürüm için OP'leri veya (tercihen) genel terimleri kullanın, 2) OP'nin Q'su, dizelerin nasıl "birleştirileceği / toplanacağı " olduğundan ve bu yalnızca dizeler için gereklidir ( sayısal bir değere kıyasla), genel terimler çok geneldir. Sadece "GroupNumber" ve "StringValue" kullanın, 3) Bir "Sınırlayıcı" Değişkeni bildirin ve kullanın ve "Len (Sınırlayıcı)" ile "2" yi kullanın.
Tom

1
Özel karakteri XML kodlamasına genişletmemek için +1 (ör. "&" Diğer pek çok alt çözümde olduğu gibi "& amp;" olarak genişletilmez)
Reversed Engineer

16

Güncelleştirme: Ms SQL Server 2017+, Azure SQL Veritabanı

Sen kullanabilirsiniz: STRING_AGG.

OP'nin talebi için kullanım oldukça basittir:

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

Daha fazla oku

Eski yanıtlamamışım haklı olarak silindi (aşağıda gözden geçirilmemiştir), ancak gelecekte buraya inecek biri olursa, iyi haberler var. Azure SQL Veritabanında da STRING_AGG () uygulamasına sahipler. Bu, bu yayında orijinal olarak istenen işlevselliği yerel ve yerleşik destekle sağlamalıdır. @hrobky bundan daha önce bir SQL Server 2016 özelliği olarak bahsetmişti.

--- Eski Gönderi: Burada @ hrobky'ye doğrudan yanıt vermek için yeterli itibar yok, ancak STRING_AGG harika görünüyor, ancak şu anda yalnızca SQL Server 2016 vNext'te mevcut. Umarım yakında Azure SQL Datababse'a da gelir ..


2
Henüz test ettim ve Azure SQL Veritabanında bir cazibe gibi çalışıyor
milano

4
STRING_AGG()SQL Server 2017'de herhangi bir uyumluluk düzeyinde kullanıma sunulacağı belirtilmektedir. docs.microsoft.com/en-us/sql/t-sql/functions/…
kullanıcı

1
Evet. STRING_AGG, SQL Server 2016'da kullanılamaz.
Magne

2

Dizeleri birleştirmek için + = kullanabilirsiniz, örneğin:

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

@test'i seçerseniz, size sıralı olarak tüm adları verir


Lütfen desteklendiği zamandan beri SQL lehçesi veya sürümünü belirtin.
Hrobky

Bu, SQL Server 2012'de çalışır. Virgülle ayrılmış bir listeninselect @test += name + ', ' from names
Art Schmidt

4
Bu, tanımlanmamış davranış kullanır ve güvenli değildir. ORDER BYSorgunuzda bir tane varsa, bu özellikle garip / yanlış bir sonuç verecektir . Listelenen alternatiflerden birini kullanmalısınız.
Dannnno

1
Bu tür bir sorgu hiçbir zaman tanımlanmış davranış değildi ve SQL Server 2019'da yanlış davranışa önceki sürümlerden daha tutarlı bir şekilde sahip olduğunu gördük. Bu yaklaşımı kullanmayın.
Matthew Rodatus

2

Serge'nin cevabını çok ümit verici buldum, ancak yazıldığı şekliyle performans sorunlarıyla da karşılaştım. Ancak, geçici tablolar kullanmak ve çift CTE tabloları içermemek için yeniden yapılandırdığımda, performans 1000 birleşik kayıt için 1 dakika 40 saniyeden saniyenin altına düştü. Burada, SQL Server'ın eski sürümlerinde FOR XML olmadan bunu yapması gereken herkes içindir:

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
;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.