一時テーブルは、熱心なスプールよりもハロウィーンの問題に対するより効率的なソリューションであるのはなぜですか?


14

行がターゲットテーブルにない場合にのみソーステーブルから行を挿入する次のクエリを検討してください。

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

考えられる1つの形状には、マージ結合と積極的なスプールが含まれます。ハロウィーンの問題を解決するために、熱心なスプールオペレーターがいます。

最初の計画

私のマシンでは、上記のコードは約6900ミリ秒で実行されます。テーブルを作成するための再現コードは、質問の下部に含まれています。パフォーマンスに不満がある場合は、熱心なスプールに頼るのではなく、一時テーブルに挿入される行をロードしようとするかもしれません。可能な実装の1つを次に示します。

DROP TABLE IF EXISTS #CONSULTANT_RECOMMENDED_TEMP_TABLE;
CREATE TABLE #CONSULTANT_RECOMMENDED_TEMP_TABLE (
    ID BIGINT,
    PRIMARY KEY (ID)
);

INSERT INTO #CONSULTANT_RECOMMENDED_TEMP_TABLE WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1);

新しいコードは約4400ミリ秒で実行されます。実際の計画を取得し、Actual Time Statistics™を使用して、オペレーターレベルでどこに時間が費やされているかを調べることができます。実際のプランを要求すると、これらのクエリに大きなオーバーヘッドが追加されるため、合計が以前の結果と一致しないことに注意してください。

╔═════════════╦═════════════╦══════════════╗
  operator    first query  second query 
╠═════════════╬═════════════╬══════════════╣
 big scan     1771         1744         
 little scan  163          166          
 sort         531          530          
 merge join   709          669          
 spool        3202         N/A          
 temp insert  N/A          422          
 temp scan    N/A          187          
 insert       3122         1545         
╚═════════════╩═════════════╩══════════════╝

熱心なスプールを使用したクエリプランは、一時テーブルを使用するプランと比較して、挿入演算子とスプール演算子にかなり多くの時間を費やすようです。

一時テーブルを使用したプランの方が効率的なのはなぜですか?とにかく、熱心なスプールはほとんど内部の一時テーブルではありませんか?内部に焦点を当てた答えを探していると思います。コールスタックがどのように異なるかはわかりますが、全体像はわかりません。

誰かが知りたい場合に備えて、SQL Server 2017 CU 11を使用しています。上記のクエリで使用されるテーブルを設定するコードは次のとおりです。

DROP TABLE IF EXISTS dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR;

CREATE TABLE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR (
ID BIGINT NOT NULL,
PRIMARY KEY (ID)
);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (20000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.A_HEAP_OF_MOSTLY_NEW_ROWS;

CREATE TABLE dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (
ID BIGINT NOT NULL
);

INSERT INTO dbo.A_HEAP_OF_MOSTLY_NEW_ROWS WITH (TABLOCK)
SELECT TOP (1900000) 19999999 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

回答:


14

これは私が手動ハロウィーン保護と呼ぶものです。

私の記事「更新クエリの最適化」で、更新ステートメントで使用されている例を見つけることができます。同じセマンティクスを維持するために、たとえば、個別のクエリが実行されている間、すべての同時変更に対してターゲットテーブルをロックするなど、シナリオに関連する場合は、少し注意する必要があります。

一時テーブルを使用したプランの方が効率的なのはなぜですか?とにかく、熱心なスプールはほとんど内部の一時テーブルではありませんか?

スプールには一時テーブルの特性の一部がありますが、この2つは完全に同等ではありません。特に、スプールは基本的に、bツリー構造へのごとの順序なし挿入です。ロックとロギングの最適化の恩恵は受けますが、バルクロードの最適化はサポートしていません。

そのため、クエリを自然な方法で分割することにより、パフォーマンスを向上させることができます。新しい行を一時テーブルまたは変数に一括読み込みし、一時オブジェクトから最適化挿入(明示的なハロウィーン保護なし)を実行します。

この分離を行うと、元のステートメントの読み取り部分と書き込み部分を個別に調整する自由度がさらに高まります。

補足として、行バージョンを使用してハロウィーンの問題にどのように対処できるかを考えるのは興味深いことです。おそらく、SQL Serverの将来のバージョンは、適切な状況でその機能を提供するでしょう。


Michael Kutzがコメントでほのめかしたように、明示的なHPを回避するために穴埋め最適化を活用する可能性を探ることもできます。デモでこれを実現する1つの方法は、のID列に一意のインデックス(必要に応じてクラスター化)を作成することですA_HEAP_OF_MOSTLY_NEW_ROWS

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);

その保証が適切に行われると、オプティマイザーは穴埋めと行セット共有を使用できます。

MERGE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (SERIALIZABLE) AS HICETY
USING dbo.A_HEAP_OF_MOSTLY_NEW_ROWS AS AHOMNR
    ON AHOMNR.ID = HICETY.ID
WHEN NOT MATCHED BY TARGET
THEN INSERT (ID) VALUES (AHOMNR.ID);

マージ計画

おもしろいですが、慎重に実装された手動ハロウィーン保護を採用することで、多くの場合、パフォーマンスを向上させることができます。


5

Paulの回答を少し拡大すると、スプールと一時テーブルのアプローチの経過時間の違いの一部はDML Request Sort、スプール計画のオプションのサポートが不足しているためと思われます。文書化されていないトレースフラグ8795を使用すると、一時テーブルアプローチの経過時間は4400ミリ秒から5600ミリ秒にジャンプします。

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1, QUERYTRACEON 8795);

これは、スプール計画によって実行される挿入と正確に同等ではないことに注意してください。このクエリは、かなり多くのデータをトランザクションログに書き込みます。

同じ効果が逆に、いくつかのトリックで見られます。ハロウィーン保護のために、SQL Serverがスプールではなくソートを使用するように奨励することは可能です。1つの実装:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (987654321) 
maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
ORDER BY maybe_new_rows.ID, maybe_new_rows.ID + 1
OPTION (MAXDOP 1, QUERYTRACEON 7470, MERGE JOIN);

これで、プランにはスプールの代わりにTOP Nソート演算子があります。ソートはブロッキング演算子なので、スプールは不要になります。

ここに画像の説明を入力してください

さらに重要なことは、このDML Request Sortオプションをサポートできるようになったことです。実際の時間統計をもう一度見ると、挿入演算子は1623ミリ秒しかかかりません。計画全体は、実際の計画を要求せずに実行するのに約5400ミリ秒かかります。

Hugoが説明するように、Eager Spoolオペレーターは順序を保持します。それはTOP PERCENT計画で最も簡単に見ることができます。スプールを使用した元のクエリが、スプール内のデータのソートされた性質をうまく活用できないのは残念です。

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