次の工夫された単純なクエリを考えてみましょう:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP;
このクエリの最終的な行の見積もりは、X_HEAP
テーブルの行数と同じになると思います。行をフィルターで除外できないため、サブクエリで何を行っていても、行の見積もりには関係ありません。ただし、SQL Server 2016では、サブクエリのために行の推定値が1に減少しています。
なぜこれが起こるのですか?それについて私は何ができますか?
この問題を正しい構文で再現するのは非常に簡単です。これを行う一連のテーブル定義を次に示します。
CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);
INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;
CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;
dbフィドル リンク 。
このカーディナリティ推定(CE)の問題は、次の場合に発生します。
注:選択度の決定に使用される特定の計算機は重要ではありません。
CEは、外部結合の選択性をsumとして計算します。
外部結合と内部結合の唯一の違いは、外部結合も結合述語で一致しない行を返すことです。アンチ結合は、まさにこの違いを提供します。内部結合と反結合のカーディナリティの推定は、外部結合を直接行うよりも簡単です。
結合選択性推定プロセスは非常に簡単です。
SPT
パススルー述語の評価されます。IsFalseOrNull
コンポーネントを含む全体です。1 - SPT
SPT
逆結合は、結合を「通過」する行を表します。内部結合は、「通過」しない行を表します。 「通過」とは、内側をまったく実行せずに結合を通過する行を意味することに注意してください。強調すると、すべての行が結合によって返されます。違いは、出現する前に結合の内側を実行する行と出現しない行の間の違いです。
明らかに、追加 1 - SPT
に SPT
常に合計選択性が1になるはずです。つまり、すべての行が期待どおりに結合によって返されます。
実際、上記の計算は、SPT
1を除くのすべての値について説明したとおりに機能します。
SPT
= 1の場合、内部結合と反結合の選択性はどちらもゼロと推定され、その結果、1つの行の(結合全体の)カーディナリティの推定が行われます。私の知る限り、これは意図的なものではなく、バグとして報告する必要があります。
このバグは、CEの制限があるため、考えられるよりも顕在化する可能性が高くなります。これは、CASE
式がEXISTS
句を使用する場合に発生します(一般的です)。たとえば、質問からの次の変更されたクエリはnotを実行して、予期しないカーディナリティの見積もりを検出します。
-- This is fine
SELECT
CASE
WHEN XH.ID = 1
THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT)
END
FROM dbo.X_HEAP AS XH;
ささいなEXISTS
を導入すると、問題が表面化します。
-- This is not fine
SELECT
CASE
WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT)
END
FROM dbo.X_HEAP AS XH;
EXISTS
を使用すると、実行計画に準結合(強調表示)が導入されます。
準結合の見積もりは問題ありません。問題は、CEが関連付けられたプローブカラムを、1の固定された選択性を持つ単純な投影として扱うことです。
Semijoin with probe column treated as a Project.
Selectivity of probe column = 1
これは、EXISTS
句の内容に関係なく、このCEの問題が明らかになるために必要な条件の1つを自動的に満たします。
重要な背景情報については、Craig Freedmanによる CASE
式のサブクエリ を参照してください。
これは明らかに意図しない動作のようです。カーディナリティの見積もりは、プランの各ステップで一貫している必要はありませんが、これは比較的単純なクエリプランであり、最終的なカーディナリティの見積もりは、クエリの実行内容と一致していません。このようにカーディナリティの見積もりが低いと、より複雑な計画で、下流にある他のテーブルの結合タイプとアクセス方法の選択が不適切になる可能性があります。
試行錯誤により、問題が発生しない同様のクエリをいくつか作成できます。
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
問題が発生するクエリをさらに作成することもできます。
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
パターンがあるように見えます。CASE
内に実行が予期されていない式があり、結果の式がテーブルに対するサブクエリである場合、行の推定値はその式の後で1になります。
クラスタ化インデックスのあるテーブルに対してクエリを作成すると、ルールが多少変更されます。同じデータを使用できます。
CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))
INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;
UPDATE STATISTICS X_CI WITH FULLSCAN;
このクエリには、1000行の最終的な見積もりがあります。
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI;
しかし、このクエリには1行の最終的な見積もりがあります。
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI;
これをさらに掘り下げるには、ドキュメント化されていない トレースフラグ236 を使用して、クエリオプティマイザが選択性計算を実行した方法に関する情報を取得します。そのトレースフラグとドキュメントに記載されていない トレースフラグ8606 を組み合わせると便利です。 TF 2363は、単純化されたツリーとプロジェクトの正規化後のツリーの両方に対して選択性計算を行うようです。両方のトレースフラグを有効にすると、どの計算がどのツリーに適用されるかが明確になります。
質問に投稿された元のクエリで試してみましょう:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
これは、いくつかのコメントとともに、関連すると思われる出力の一部です。
Plan for computation:
CSelCalcColumnInInterval -- this is the type of calculator used
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation
Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)
次に、問題のない同様のクエリを試してみましょう。これを使用します。
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
最後のデバッグ出力:
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table
不良な行の見積もりが存在する別のクエリを試してみましょう。
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
最後に、やはりパススルー選択性= 1の後、カーディナリティ推定値は1行に下がります。カーディナリティ推定値は、選択性0.501および0.499の後も保持されます。
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.501
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression
CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)
もう一度、問題のない別の同様のクエリに切り替えましょう。これを使用します。
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
デバッグ出力では、パススルー選択性が1のステップはありません。カーディナリティの推定値は1000行のままです。
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
End selectivity computation
クラスタ化インデックス付きのテーブルが関係するクエリについてはどうですか?行推定の問題がある次のクエリについて考えます。
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
デバッグ出力の終わりは、すでに見たものに似ています:
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_CI].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
ただし、問題のないCIに対するクエリの出力は異なります。このクエリを使用する:
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
異なる電卓が使用されます。 CSelCalcColumnInInterval
は表示されなくなりました:
Plan for computation:
CSelCalcFixedFilter (0.559)
Pass-through selectivity: 0.559
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
...
Plan for computation:
CSelCalcUniqueKeyFilter
Pass-through selectivity: 0.001
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)
結論として、次の条件下では、サブクエリの後に不適切な行の見積もりが表示されるようです。
CSelCalcColumnInInterval
選択性計算機が使用されます。これがいつ使用されるかは正確にはわかりませんが、ベーステーブルがヒープである場合、より頻繁に表示されるようです。
パススルー選択性=1。つまり、CASE
式の1つがすべての行でfalseと評価されることが期待されます。最初のCASE
式がすべての行でtrueと評価されるかどうかは問題ではありません。
CStCollBaseTable
への外部結合があります。つまり、CASE
結果式は、テーブルに対するサブクエリです。定数値は機能しません。
おそらくこれらの条件下では、クエリオプティマイザーは、ネストされたループの内部で行われた作業ではなく、外部テーブルの行推定にパススルー選択性を誤って適用しています。これにより、行の推定が1に減少します。
2つの回避策を見つけることができました。サブクエリの代わりにAPPLY
を使用すると、問題を再現できませんでした。トレースフラグ2363の出力は、APPLY
とは大きく異なりました。質問の元のクエリを書き換える1つの方法を次に示します。
SELECT
h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END
) a(ID2);
レガシーCEもこの問題を回避しているようです。
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));
接続項目 がこの問題に対して送信されました(ポールホワイトが回答に提供した詳細の一部を含む)。