サブクエリが行の推定値を1に減らすのはなぜですか?


26

次の不自然な単純なクエリを検討してください。

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フィドルリンク

回答:


22

このカーディナリティ推定(CE)の問題は、次の場合に表面化します。

  1. 結合は、パススルー述部を持つ外部結合です
  2. パススルー述部の選択性は、正確に1と推定されます。

注:選択性の決定に使用される特定の計算機は重要ではありません。


詳細

CEは、次の合計として外部結合の選択性を計算します。

  • インナーには参加同じ述語と選択性を
  • 抗加入同じ述語と選択性を

外部結合と内部結合の唯一の違いは、外部結合も結合述部で一致しない行を返すことです。アンチ結合は、まさにこの違いを提供します。内部結合および反結合のカーディナリティー推定は、外部結合を直接行うよりも簡単です。

結合選択性の推定プロセスは非常に簡単です。

  • 最初に、パススルー述部の選択性が評価されます。 SPT
    • これは、状況に適した計算機を使用して行われます。
    • 述部は、否定IsFalseOrNullコンポーネントを含む全体です。
  • 内部結合の選択性:= 1 - SPT
  • アンチ結合選択性:= SPT

反結合は、結合を「パススルー」する行を表します。内部結合は、「パススルー」されない行を表します。「パススルー」とは、内側をまったく実行せずに結合を通過する行を意味することに注意してください。強調するのは、すべての行が結合によって返されることです。区別は、出現する前に結合の内側を実行する行とそうでない行との間です。

明らかに、に追加すると、常に1の合計選択性が得られるはずです。つまり、期待どおり、すべての行が結合によって返されます。1 - SPTSPT

実際、上記の計算は、1以外のすべての値について説明したとおりに機能します。SPT

場合= 1、両方の内側には、参加及びアンチ選択性は、1行のカーディナリティー推定値(全体として参加するために)、その結果、ゼロであると推定されている加入。私が知る限り、これは意図的なものではなく、バグとして報告されるべきです。SPT


関連する問題

このバグは、別のCE制限により、考えられるよりも顕在化する可能性が高くなります。これは、CASE式がEXISTS句を使用する場合に発生します(一般的です)。たとえば、質問からの次の変更されたクエリでは、予期しないカーディナリティの推定値は検出されませ

-- 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参照してください。


22

これは間違いなく意図しない動作のようです。カーディナリティの推定値は、プランの各ステップで一貫している必要はありませんが、これは比較的単純なクエリプランであり、最終的なカーディナリティの推定値はクエリの実行内容と一致しません。このようなカーディナリティの推定値が低いと、より複雑なプランでダウンストリームの他のテーブルの結合タイプとアクセス方法の選択が不適切になる可能性があります。

試行錯誤を通じて、問題が発生していない類似のクエリをいくつか見つけることができます。

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;

これをさらに掘り下げるには、文書化されていないトレースフラグ2363を使用して、クエリオプティマイザーが選択性の計算を実行した方法に関する情報を取得します。そのトレースフラグと文書化されていないトレースフラグ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)

結論として、次の条件下では、サブクエリの後に不良な行の推定値が得られるようです。

  1. CSelCalcColumnInInterval選択性の計算が使用されています。これがいつ使用されるかは正確にはわかりませんが、ベーステーブルがヒープの場合はずっと頻繁に表示されるようです。

  2. パススルー選択性=1。つまり、CASE式の1つは、すべての行に対してfalseと評価されることが期待されます。最初のCASE式がすべての行でtrueと評価されるかどうかは関係ありません。

  3. への外部結合があり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);

良いクエリ1

従来の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'));

良いクエリ2

接続項目は(ポール・ホワイトは彼の答えに提供したことの詳細のいくつかと)この問題のために提出されました。

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