SQL Server 2005以降、FOR XML PATH
を使用するコツ 文字列を非正規化し、それらを1つの(通常はコンマ区切りの)リストに結合することは非常に人気があります。ただし、SQL Server 2017では、STRING_AGG()
GROUP_CONCAT()
をシミュレートするために、コミュニティからの長年にわたる広範な嘆願に最終的に答えました および他のプラットフォームにある同様の機能。最近、既存のコードを改善し、最新バージョンにより適した例を追加するために、古い方法を使用してStackOverflowの回答の多くを変更し始めました。
見つけたものに少し愕然としました。
何度か、コードが自分のものであるかどうかを再確認する必要がありました。
簡単な例
問題の簡単なデモンストレーションを見てみましょう。誰かがこのようなテーブルを持っています:
CREATE TABLE dbo.FavoriteBands ( UserID int, BandName nvarchar(255) ); INSERT dbo.FavoriteBands ( UserID, BandName ) VALUES (1, N'Pink Floyd'), (1, N'New Order'), (1, N'The Hip'), (2, N'Zamfir'), (2, N'ABBA');
各ユーザーのお気に入りのバンドを表示するページで、ユーザーは出力を次のようにしたいと考えています。
UserID Bands ------ --------------------------------------- 1 Pink Floyd, New Order, The Hip 2 Zamfir, ABBA
SQL Server 2005の時代には、私はこのソリューションを提供していました:
SELECT DISTINCT UserID, Bands = (SELECT BandName + ', ' FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH('')) FROM dbo.FavoriteBands AS fb;
しかし、今このコードを振り返ると、修正に抵抗できない多くの問題があります。
STUFF
上記のコードの最も致命的な欠陥は、末尾にコンマが残ることです:
UserID Bands ------ --------------------------------------- 1 Pink Floyd, New Order, The Hip, 2 Zamfir, ABBA,
これを解決するために、人々がクエリを別のクエリにラップしてから、Bands
を囲むのをよく目にします。 LEFT(Bands, LEN(Bands)-1)
で出力 。しかし、これは不必要な追加の計算です。代わりに、STUFF
を使用して、カンマを文字列の先頭に移動し、最初の1文字または2文字を削除できます。 。そうすれば、文字列は無関係なので、文字列の長さを計算する必要はありません。
SELECT DISTINCT UserID, Bands = STUFF( --------------------------------^^^^^^ (SELECT ', ' + BandName --------------^^^^^^ FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH('')), 1, 2, '') --------------------------^^^^^^^^^^^ FROM dbo.FavoriteBands AS fb;
より長い区切り文字または条件付き区切り文字を使用している場合は、これをさらに調整できます。
DISTINCT
次の問題は、DISTINCT
の使用です。 。コードが機能する方法は、派生テーブルがUserID
ごとにコンマ区切りのリストを生成することです。 値の場合、重複は削除されます。これは、プランを見て、最終的に3行しか返されないにもかかわらず、XML関連の演算子が7回実行されることでわかります。
図1:集計後のフィルターを示す計画
GROUP BY
を使用するようにコードを変更した場合 DISTINCT
の代わりに :
SELECT /* DISTINCT */ UserID, Bands = STUFF( (SELECT ', ' + BandName FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH('')), 1, 2, '') FROM dbo.FavoriteBands AS fb GROUP BY UserID; --^^^^^^^^^^^^^^^
これは微妙な違いであり、結果は変わりませんが、計画が改善されていることがわかります。基本的に、XML操作は、重複が削除されるまで延期されます。
図2:集計前のフィルターを示す計画
このスケールでは、違いは重要ではありません。しかし、さらにデータを追加するとどうなるでしょうか。私のシステムでは、これにより11,000行強が追加されます:
INSERT dbo.FavoriteBands(UserID, BandName) SELECT [object_id], name FROM sys.all_columns;
2つのクエリを再度実行すると、期間とCPUの違いがすぐにわかります。
図3:DISTINCTとGROUPBYを比較した実行時の結果
しかし、他の副作用も計画で明らかです。 DISTINCT
の場合 、UDXはテーブル内のすべての行に対して再度実行され、過度に熱心なインデックススプールがあり、明確な並べ替えがあり(常に私にとっては危険信号です)、クエリには高いメモリ許可があり、同時実行性に深刻な打撃を与える可能性があります:
図4:大規模なDISTINCT計画
一方、GROUP BY
クエリの場合、UDXは一意のUserID
ごとに1回だけ実行されます 、熱心なスプールが読み取る行数ははるかに少なく、明確な並べ替え演算子はなく(ハッシュ一致に置き換えられています)、メモリの付与は比較的小さいです:
図5:大規模なGROUPBYプラン
このような古いコードに戻って修正するにはしばらく時間がかかりますが、しばらくの間、私は常にGROUP BY
を使用するように非常に管理されてきました。 DISTINCT
の代わりに 。
Nプレフィックス
私が遭遇した古いコードサンプルが多すぎると、Unicode文字が使用されることはないと想定されていました。または、少なくともサンプルデータはその可能性を示唆していませんでした。上記のようにソリューションを提供すると、ユーザーが戻ってきて、「しかし、1つの行に'просто красный'
があります。 、そしてそれは'?????? ???????'
!」 varchar
のみを扱うことを絶対に知らない限り、潜在的なUnicode文字列リテラルの前にNプレフィックスを付ける必要があることをよく思い出します。 文字列または整数。私は非常に露骨になり始め、おそらくそれについては慎重にさえなりました:
SELECT UserID, Bands = STUFF( (SELECT N', ' + BandName --------------^ FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH(N'')), 1, 2, N'') ----------------------^ -----------^ FROM dbo.FavoriteBands AS fb GROUP BY UserID;
XMLエンティティ化
別の「もしも?」ユーザーのサンプルデータに常に存在するとは限らないシナリオは、XML文字です。たとえば、私のお気に入りのバンドの名前が「Bob & Sheila <> Strawberries
」の場合はどうなりますか? 」?上記のクエリの出力はXMLセーフになりますが、これは常に必要なものではありません(例:Bob & Sheila <> Strawberries
)。当時のGoogle検索では、「TYPE
を追加する必要があります 、」そして私はこのようなことを試みたことを覚えています:
SELECT UserID, Bands = STUFF( (SELECT N', ' + BandName FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH(N''), TYPE), 1, 2, N'') --------------------------^^^^^^ FROM dbo.FavoriteBands AS fb GROUP BY UserID;
残念ながら、この場合のサブクエリからの出力データ型はxml
です。 。これにより、次のエラーメッセージが表示されます。
引数データ型xmlは、スタッフ関数の引数1では無効です。
データ型を指定して結果の値を文字列として抽出することと、最初の要素が必要であることをSQLServerに通知する必要があります。当時、私はこれを次のように追加していました:
SELECT UserID, Bands = STUFF( (SELECT N', ' + BandName FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), --------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1, 2, N'') FROM dbo.FavoriteBands AS fb GROUP BY UserID;
これにより、XMLエンティティ化なしで文字列が返されます。しかし、それは最も効率的ですか?昨年、チャーリーフェイスは、近眼のマグーがいくつかの広範なテストを実行し、./text()[1]
を見つけたことを思い出しました .
および.[1]
。 (これは、Mikael Erikssonがここに残したコメントから最初に聞いたものです。)コードをもう一度次のように調整しました:
SELECT UserID, Bands = STUFF( (SELECT N', ' + BandName FROM dbo.FavoriteBands WHERE UserID = fb.UserID FOR XML PATH(N''), TYPE).value(N'./text()[1]', N'nvarchar(max)'), ------------------------------------------^^^^^^^^^^^ 1, 2, N'') FROM dbo.FavoriteBands AS fb GROUP BY UserID;
この方法で値を抽出すると、少し複雑な計画につながることがわかります(上記の変更を通じてかなり一定に保たれている期間を見ただけではわかりません):
図6:./text()[1]を使用した計画
ルートの警告SELECT
演算子は、nvarchar(max)
への明示的な変換に由来します 。
注文
時折、ユーザーは順序付けが重要であると表現します。多くの場合、これは単に追加する列の順序ですが、別の場所に追加できる場合もあります。人々は、特定の注文がSQL Serverから出てくるのを見た場合、それは常に表示される順序であると信じがちですが、ここでは信頼性がありません。あなたがそう言わない限り、注文は決して保証されません。この場合、BandName
で注文するとします。 アルファベット順。この命令をサブクエリ内に追加できます:
SELECT UserID, Bands = STUFF( (SELECT N', ' + BandName FROM dbo.FavoriteBands WHERE UserID = fb.UserID ORDER BY BandName ---------^^^^^^^^^^^^^^^^^ FOR XML PATH(N''), TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 2, N'') FROM dbo.FavoriteBands AS fb GROUP BY UserID;
サポートするインデックスがあるかどうかによっては、並べ替え演算子が追加されるため、実行時間が少し長くなる可能性があることに注意してください。
STRING_AGG()
質問の時点で関連していたバージョンで引き続き機能するはずの古い回答を更新すると、上記の最後のスニペット(ORDER BY
の有無にかかわらず) )は、おそらく表示されるフォームです。ただし、より新しい形式の追加の更新も表示される場合があります。
STRING_AGG()
は間違いなくSQLServer2017で追加された最高の機能の1つです。これは、上記のどのアプローチよりもシンプルではるかに効率的であり、次のようなきちんとしたパフォーマンスの高いクエリにつながります。
SELECT UserID, Bands = STRING_AGG(BandName, N', ') FROM dbo.FavoriteBands GROUP BY UserID;
これは冗談ではありません。それでおしまい。計画は次のとおりです。最も重要なのは、テーブルに対するスキャンが1回だけであるということです。
図7:STRING_AGG()プラン
注文する場合は、STRING_AGG()
これもサポートします(Martin Smithがここで指摘しているように、互換性レベル110以上である限り):
SELECT UserID, Bands = STRING_AGG(BandName, N', ') WITHIN GROUP (ORDER BY BandName) ----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FROM dbo.FavoriteBands GROUP BY UserID;
計画は見える 並べ替えなしのものと同じですが、私のテストではクエリが少し遅くなります。それでも、FOR XML PATH
のどれよりもはるかに高速です バリエーション。
インデックス
ヒープはほとんど公平ではありません。クエリで使用できる非クラスター化インデックスさえあれば、プランはさらに良く見えます。例:
CREATE INDEX ix_FavoriteBands ON dbo.FavoriteBands(UserID, BandName);
STRING_AGG()
を使用した同じ順序のクエリの計画は次のとおりです -スキャンを注文できるため、並べ替え演算子がないことに注意してください:
図8:サポートインデックスを使用したSTRING_AGG()プラン
これにより、ある程度の時間が短縮されますが、公平を期すために、このインデックスはFOR XML PATH
に役立ちます。 バリエーションも。そのクエリの注文バージョンの新しい計画は次のとおりです。
図9:サポートインデックスを使用したXMLPATHプランの場合
計画は以前よりも少し友好的で、1つのスポットでのスキャンの代わりにシークが含まれていますが、このアプローチはSTRING_AGG()
よりもかなり低速です。 。
警告
STRING_AGG()
を使用するためのちょっとしたコツがあります ここで、結果の文字列が8,000バイトを超える場合は、次のエラーメッセージが表示されます。
STRING_AGG集計結果が8000バイトの制限を超えました。結果の切り捨てを回避するには、LOBタイプを使用してください。
この問題を回避するために、明示的な変換を挿入できます:
SELECT UserID, Bands = STRING_AGG(CONVERT(nvarchar(max), BandName), N', ') --------------------------^^^^^^^^^^^^^^^^^^^^^^ FROM dbo.FavoriteBands GROUP BY UserID;
これにより、計算スカラー演算がプランに追加され、驚くことではないCONVERT
ルートの警告SELECT
オペレーター—ただし、それ以外の場合、パフォーマンスへの影響はほとんどありません。
結論
SQL Server 2017以降を使用していて、FOR XML PATH
がある場合 コードベースでの文字列集約。新しいアプローチに切り替えることを強くお勧めします。 SQL Server 2017のパブリックプレビュー中に、ここでさらに徹底的なパフォーマンステストを実行しました。ここで、もう一度確認してください。
私が聞いた一般的な反対意見は、SQL Server 2017以降を使用しているが、それでも古い互換性レベルを使用しているというものです。 STRING_SPLIT()
が原因のようです。 130未満の互換性レベルでは無効であるため、STRING_AGG()
と見なされます。 このようにも機能しますが、もう少し寛大です。 WITHIN GROUP
を使用している場合にのみ問題になります および 互換性レベルが110未満です。改善してください!