文字列を連結/集約する最適な方法


102

異なる行の文字列を1つの行に集約する方法を見つけています。私はさまざまな場所でこれを行うことを考えているので、これを容易にする機能があると便利です。私が使用して解決策を試してみたCOALESCEFOR XML、彼らはちょうど私のためにそれをカットしないでください。

文字列の集約は次のようになります。

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

私は見ました CLR定義の集約関数の代替としてのCOALESCEFOR XML、どうやらSQL Azureが ありません、私はそれを使用することができるということの全体の多くを解決するだろう知っているので、私にとっては苦痛であるCLR-定義されたものを、サポート私にとっての問題。

すべての可能な回避策はありますか(CLRとして最適とはならないかもしれないが、同様に最適な方法ちょっと私は私のものを集約するために使用できることを、私は私が得ることができるもの取りますよ)?


どのようにしてfor xmlうまくいきませんか?
ミカエルエリクソン

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

回答:


67

解決

最適の定義はさまざまですが、通常の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

説明

アプローチは、3つのステップに要約されます。

  1. 連結の必要に応じて、行の番号付け、OVERおよびPARTITIONグループ化と順序付けを行います。結果はPartitionedCTEです。結果を後でフィルタリングするために、各パーティションの行数を保持します。

  2. 再帰CTE(Concatenated)を使用すると、行番号(NameNumbercolumn)を反復処理してName値をFullNamecolumnに追加します。

  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に対してチェックしたところ、約54ミリ秒ではなく約4ミリ秒に達しました。したがって、xmplathの方法は、特に大きなケースで特に優れています。比較コードは別の答えで書きます。
QMaster

このアプローチは最大100個の値に対してのみ機能するため、はるかに優れています。
RomanoZumbé2014年

@romano-zumbéMAXRECURSIONを使用して、CTE制限を必要なものに設定します。
Serge Belov 2014年

1
驚いたことに、CTEは私にとってはかなり遅いものでした。sqlperformance.com/2014/08/t-sql-queries/…は一連の手法を比較し、私の結果に同意するようです。
Nickolay 2016年

100万を超えるレコードがあるテーブルに対するこのソリューションは機能しません。また、再帰的な深さには制限があります
Ardalan Shahgholi

51

以下のようなFOR XML PATHを使用するメソッドは本当に遅いのですか?Itzik Ben-Ganは、彼のT-SQLクエリブックでこのメソッドのパフォーマンスが優れていると書いています(Ben-Gan氏は私の見解では信頼できる情報源です)。

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

1
そして、stuff / for xmlパスがどのように機能するか(stackoverflow.com/a/31212160/1026)を読んだ後、名前にXMLがあるにもかかわらず、それが良い解決策であると確信しています:)
Nickolay

1
@slackterman操作するレコードの数によって異なります。XMLはCTEに比べて数が少ないと不足していると思いますが、量が多いと、再帰部の制限が緩和され、正確かつ簡潔に行えばナビゲートが容易になります。
GoldBishop 2017年

データに絵文字や特殊な/サロゲート文字があると、FOR XML PATHメソッドは爆破します!!!
devinbost 2017年

1
このコードは、結果がxmlエンコードされたテキストになります(などに&切り替えられます&)。より正確なfor xml解決策がここに提供されています
フレデリック

33

これを見つけた私たちのために Azure SQL Databaseを使用していない

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/ functions / string-agg-transact-sql

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

(Azure更新の@Brianjordenと@milanioに感謝)

コード例:

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

SQLフィドル:http : //sqlfiddle.com/#!18/89251/1


1
私はそれをテストしたばかりで、Azure SQL Databaseで正常に動作します。
milanio 2017年

5
STRING_AGGこれは、2016年には利用できません戻って2017年に押されてしまった
モルガンThrapp

1
SQL Serverのバージョン変更について、AamirとMorgan Thrappに感謝します。更新しました。(執筆時点では、バージョン2016でサポートされていると主張されていました。)
Hrobky、2017年

25

@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あなた!私はさらに劇的な差分を得ました。(Intel Xeon E5-2630 v4 @ 2.20 GHZ x2上のSQL Server 2008 R2上のSQL Server 2008 R2上の〜70ミリ秒XMLに対して、約3000ミリ秒のCTEと、約1 GBの空き容量)。提案は次のとおりです。1)両方のバージョンでOPまたは(できれば)一般的な用語を使用します。2)OPのQ.は「文字列を連結/集約する」方法であり、これは文字列数値に対して)にのみ必要です。用語が一般的すぎる。「GroupNumber」と「StringValue」を使用するだけです。3)「Delimiter」変数を宣言して使用し、「Len(Delimiter)」と「2」を使用します。
トム

1
特殊文字をXMLエンコーディングに拡張しないための+1(たとえば、他の多くの劣ったソリューションのように、「&」は「&amp;」に拡張されません)
リバースエンジニア

13

更新:Ms SQL Server 2017以降、Azure SQLデータベース

次を使用できますSTRING_AGG

OPのリクエストの使い方は非常に簡単です。

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

続きを読む

さて、私の古い非回答は正当に削除されました(下にそのまま残っています)が、もし誰かが将来ここに着陸した場合、良いニュースがあります。それらは、Azure SQLデータベースでもSTRING_AGG()を実装しています。これにより、この投稿で最初に要求された正確な機能がネイティブサポートと組み込みサポートで提供されます。@hrobkyは以前、これをSQL Server 2016の機能として以前言及していました。

---以前の投稿:@hrobkyに直接返信するのに十分な評判はありませんが、STRING_AGGは見栄えが良いですが、現在SQL Server 2016 vNextでのみ使用できます。うまくいけば、すぐにAzure SQL Datababseにも対応する予定です。


2
私はそれをテストしたところ、Azure SQLデータベースの魅力のように動作しました
ミラノ

4
STRING_AGG()SQL Server 2017で、あらゆる互換性レベルで利用可能になると記載されています。docs.microsoft.com/en-us/sql/t-sql/functions/…–
CVn

1
はい。STRING_AGGは、SQL Serverの2016年には利用できません
マグネ

2

+ =を使用して文字列を連結できます。次に例を示します。

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では、以前のバージョンよりも一貫して不正な動作が発生することがわかりました。このアプローチは使用しないでください。
マシューロダタス

2

Sergeの回答は非常に有望であることがわかりましたが、作成時のパフォーマンスの問題も発生しました。ただし、一時的なテーブルを使用し、二重のCTEテーブルを含めないように再構成すると、1000の組み合わせレコードのパフォーマンスが1分40秒から1秒未満になりました。ここでは、古いバージョンのSQL ServerでFOR XMLを使用せずにこれを行う必要があるすべての人を対象としています。

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
;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.