web-dev-qa-db-ja.com

サブクエリによって行の推定値が1に減少するのはなぜですか?

次の工夫された単純なクエリを考えてみましょう:

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に減少しています。

bad query

なぜこれが起こるのですか?それについて私は何ができますか?

この問題を正しい構文で再現するのは非常に簡単です。これを行う一連のテーブル定義を次に示します。

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

26
Joe Obbish

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

  1. 結合はouterpass-through述語による結合です
  2. パススルー述語のselectivityexactly 1と推定されます。

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


細部

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

  • 内部結合同じ述語での選択性
  • anti join同じ述語での選択性

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

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

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

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

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

実際、上記の計算は、SPT1を除くのすべての値について説明したとおりに機能します。

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を使用すると、実行計画に準結合(強調表示)が導入されます。

Semi join plan

準結合の見積もりは問題ありません。問題は、CEが関連付けられたプローブカラムを、1の固定された選択性を持つ単純な投影として扱うことです。

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

これは、EXISTS句の内容に関係なく、このCEの問題が明らかになるために必要な条件の1つを自動的に満たします。


重要な背景情報については、Craig Freedmanによる CASE式のサブクエリ を参照してください。

23
Paul White 9

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

試行錯誤により、問題が発生しない同様のクエリをいくつか作成できます。

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)

結論として、次の条件下では、サブクエリの後に不適切な行の見積もりが表示されるようです。

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

good query 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'));

good query 2

接続項目 がこの問題に対して送信されました(ポールホワイトが回答に提供した詳細の一部を含む)。

23
Joe Obbish