web-dev-qa-db-ja.com

一時テーブルが熱心なスプールよりもハロウィーン問題の効率的な解決策であるのはなぜですか?

行がまだターゲットテーブルにない場合にのみ、ソーステーブルから行を挿入する次のクエリについて考えます。

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つのプラン形状には、マージ結合と熱心なスプールがあります。熱心なスプールオペレーターは ハロウィーン問題 を解決するために存在します:

first plan

私のマシンでは、上記のコードは約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
Joe Obbish

これは私が呼ぶものですManual Halloween Protection

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

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

スプールには一時テーブルのいくつかの特性がありますが、2つは完全に同等ではありません。特に、スプールは本質的に行ごとの Bツリー構造への順不同の挿入 です。ロックとログの最適化のメリットはありますが、バルクロードの最適化はサポートしていません。

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

この分離を行うことにより、元のステートメントの読み取り部分と書き込み部分を別々に調整するための追加の自由が可能になります。

補足として、行バージョンを使用してハロウィーン問題に対処する方法について考えるのは興味深いことです。 SQL Serverの将来のバージョンでは、適切な状況でその機能が提供される可能性があります。


Michael Kutzがコメントで言及したように、明示的なHPを回避するために hole-filling最適化 を利用する可能性を探ることもできます。デモでこれを達成する1つの方法は、A_HEAP_OF_MOSTLY_NEW_ROWSID列に一意のインデックス(必要に応じてクラスター化)を作成することです。

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);

MERGE plan

興味深いことですが、慎重に実装された手動ハロウィーン保護を採用することで、多くの場合、より良いパフォーマンスを達成できます。

14
Paul White 9

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ソート演算子が計画に追加されました。ソートはブロッキングオペレーターであるため、スプールは不要になりました。

enter image description here

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

Hugo explains のように、Eager Spoolオペレーターは順序を保持します。これはTOP PERCENTプランで最も簡単に確認できます。残念ながら、スプールを使用した元のクエリでは、スプール内のデータのソートされた性質をうまく活用できません。

5
Joe Obbish