1から400までの番号が付けられた400行の次のヒープテーブルがあるとします。
DROP TABLE IF EXISTS dbo.N;
GO
SELECT
SV.number
INTO dbo.N
FROM master.dbo.spt_values AS SV
WHERE
SV.[type] = N'P'
AND SV.number BETWEEN 1 AND 400;
および次の設定:
SET NOCOUNT ON;
SET STATISTICS IO, TIME OFF;
SET STATISTICS XML OFF;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
次のSELECT
ステートメントは約6秒で完了します( demo 、 plan ):
DECLARE @n integer = 400;
SELECT
c = COUNT_BIG(*)
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE
N.number <= @n
AND N2.number <= @n
AND N3.number <= @n
OPTION
(OPTIMIZE FOR (@n = 1));
注:@The OPTIMIZE FOR
句は、さまざまな理由で発生する可能性のあるカーディナリティの誤った見積もりを含む、実際の問題の本質的な詳細をキャプチャする適切なサイズの再現を作成するためのものです。
単一行の出力がテーブルに書き込まれると、19秒かかります( デモ 、 計画 ):
DECLARE @T table (c bigint NOT NULL);
DECLARE @n integer = 400;
INSERT @T
(c)
SELECT
c = COUNT_BIG(*)
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE
N.number <= @n
AND N2.number <= @n
AND N3.number <= @n
OPTION
(OPTIMIZE FOR (@n = 1));
実行プランは、1つの行の挿入を除いて同一に見えます。
余分な時間はすべて、CPUの使用によって消費されているようです。
なぜINSERT
ステートメントはとても遅いのですか?
SQL Serverは、行レベルのロックを使用して、ループ結合の内側にあるヒープテーブルをスキャンすることを選択します。通常、フルスキャンではページレベルのロックが選択されますが、テーブルのサイズと述語の組み合わせにより、ストレージエンジンが行ロックを選択することになります。
OPTIMIZE FOR
によって意図的に導入されたカーディナリティの誤推定は、オプティマイザが期待するよりも多くの回数ヒープがスキャンされることを意味します。通常どおりにスプールします。
この要因の組み合わせは、パフォーマンスが実行時に必要なロックの数に非常に敏感であることを意味します。
SELECT
ステートメントは、コミットされていないデータを読み取る危険がない場合に 行レベルの共有ロックをスキップする (インテント共有のページレベルロックのみを取得)を可能にする最適化の恩恵を受けます。そして、行外のデータはありません。
INSERT...SELECT
ステートメントはこの最適化の恩恵を受けないため、2番目のケースでは、インテント共有のページレベルのロックと共に、毎秒数百万のRIDロックが取得および解放されます。
膨大な量のロックアクティビティが、余分なCPUと経過時間を占めています。
最も自然な回避策は、オプティマイザ(およびストレージエンジン)が適切なカーディナリティの見積もりを取得して、適切な選択を行えるようにすることです。
実際のユースケースでそれが実用的でない場合は、INSERT
およびSELECT
ステートメントを分離して、SELECT
の結果を変数に保持することができます。これにより、SELECT
ステートメントがロックスキップ最適化の恩恵を受けることができます。
分離レベルの変更は、共有ロックを取得しないか、ロックのエスカレーションが迅速に行われるようにすることによっても機能させることができます。
最後の注目点として、ドキュメント化されていないトレースフラグ8691を使用してスプールを強制的に使用することにより、最適化されたSELECT
の場合よりもさらに高速にクエリを実行できます。