web-dev-qa-db-ja.com

SQL Serverは、いくつかの行を挿入した後、結合から出力される行が少なくなると推定するのはなぜですか?

以下は、私が本番環境で遭遇したものの単純化されたバージョンです(計画が異常に多数のバッチが処理された日に壊滅的に悪化しました)。

再現は、新しいカーディナリティエスティメータで2014および2016に対してテストされています。

CREATE TABLE T1 (FromDate  DATE, ToDate DATE, SomeId INT, BatchNumber INT);

INSERT INTO T1
SELECT TOP 1000 FromDate = '2017-01-01',
                ToDate = '2017-01-01',
                SomeId = ROW_NUMBER() OVER (ORDER BY @@SPID) -1,
                BatchNumber = 1
FROM   master..spt_values v1

CREATE TABLE T2 (SomeDateTime DATETIME, SomeId INT, INDEX IX(SomeDateTime));

INSERT INTO T2
SELECT TOP 1000000 '2017-01-01',
                   ROW_NUMBER() OVER (ORDER BY @@SPID) %1000
FROM   master..spt_values v1,
       master..spt_values v2

T1には1,000行が含まれます。

FromDateToDate、およびBatchNumberは、すべて同じです。異なる唯一の値はSomeIdで、値は0999の間です。

+------------+------------+--------+-----------+
|  FromDate  |   ToDate   | SomeId | BatchNumber |
+------------+------------+--------+-----------+
| 2017-01-01 | 2017-01-01 |      0 |         1 |
| 2017-01-01 | 2017-01-01 |      1 |         1 |
....
| 2017-01-01 | 2017-01-01 |    998 |         1 |
| 2017-01-01 | 2017-01-01 |    999 |         1 |
+------------+------------+--------+-----------+

T2には100万行が含まれます

しかし、1,000の異なるものだけです。以下のようにそれぞれ1,000回繰り返しました。

+-------------------------+--------+-------+
|      SomeDateTime       | SomeId | Count |
+-------------------------+--------+-------+
| 2017-01-01 00:00:00.000 |      0 |  1000 |
| 2017-01-01 00:00:00.000 |      1 |  1000 |
...
| 2017-01-01 00:00:00.000 |    998 |  1000 |
| 2017-01-01 00:00:00.000 |    999 |  1000 |
+-------------------------+--------+-------+

以下の実行

SELECT *
FROM   T1
       INNER JOIN T2
               ON CAST(t2.SomeDateTime AS DATE) BETWEEN T1.FromDate AND T1.ToDate
                  AND T1.SomeId = T2.SomeId
WHERE  T1.BatchNumber = 1

私のマシンでは約7秒かかります。実際の行と推定される行は、計画内のすべてのオペレーターに最適です。

enter image description here

次に、3,000の追加バッチをT1に追加します(バッチ番号2から3001)。これらはそれぞれ、バッチ番号1の既存の1000行を複製します。

INSERT INTO T1
SELECT T1.FromDate,
       T1.ToDate,
       T1.SomeId,
       Nums.NewBatchNumber
FROM   T1
       CROSS JOIN (SELECT TOP (3000) 1 + ROW_NUMBER() OVER (ORDER BY @@SPID) AS NewBatchNumber
                   FROM   master..spt_values v1, master..spt_values v2) Nums 

運の統計を更新する

 UPDATE STATISTICS T1 WITH FULLSCAN

そして、元のクエリを再度実行します。

SELECT *
FROM   T1
       INNER JOIN T2
               ON CAST(t2.SomeDateTime AS DATE) BETWEEN T1.FromDate AND T1.ToDate
                  AND T1.SomeId = T2.SomeId
WHERE  T1.BatchNumber = 1

殺す前に一分間走らせた。その時までに、40,380行を出力したので、100万を出力するのに25分かかると思います。

変更された唯一のことは、T1.BatchNumber = 1述語に一致しない行をいくつか追加したことです。

ただし、計画は変更されました。代わりにネストされたループを使用し、t1から出力される行数は引き続き1,000と正しく推定されますが(①)、結合された行数の推定値は100万から1000に減少しました(②)。

enter image description here

だから問題は...

BatchNumber <> 1を使用して行を追加すると、BatchNumber = 1

テーブルに行を追加すると、最終的にはクエリ全体の推定行数が減るので、直感に反するようです。

7
Martin Smith

クエリやテーブル内のデータを変更しても、一貫性は保証されないことに注意してください。クエリオプティマイザーは、カーディナリティ推定の異なる方法(ヒストグラムではなく密度を使用するなど)を使用して、2つのクエリが互いに矛盾しているように見せることがあります。そうは言っても、クエリオプティマイザーがあなたの場合に不当な選択をしているように思われるので、掘り下げましょう。

デモが複雑すぎるので、同じ動作を示すと思われる単純な例から作業を進めます。データ準備とテーブル定義の開始:

DROP TABLE dbo.T1 IF EXISTS;
CREATE TABLE dbo.T1 (FromDate DATE, ToDate DATE, SomeId INT);

INSERT INTO dbo.T1 WITH (TABLOCK)
SELECT TOP 1000 NULL, NULL, 1
FROM master..spt_values v1;

DROP TABLE dbo.T2 IF EXISTS;
CREATE TABLE dbo.T2 (SomeDateTime DATETIME, INDEX IX(SomeDateTime));

INSERT INTO dbo.T2 WITH (TABLOCK)
SELECT TOP 2 NULL
FROM master..spt_values v1
CROSS JOIN master..spt_values v2;

調査するSELECTクエリは次のとおりです。

SELECT *
FROM T1
INNER JOIN T2 ON t2.SomeDateTime BETWEEN T1.FromDate AND T1.ToDate
WHERE T1.SomeId = 1;

このクエリは非常に単純なので、トレースフラグなしで基数推定の式を計算できます。ただし、オプティマイザー内で何が行われているのかを詳しく説明するために、TF 2363を使用してみます。私が成功するかどうかははっきりしない。

以下の変数を定義します。

C1 =テーブルT1の行数

C2 =テーブルT2の行数

S1 = T1.SomeIdフィルターの選択性

私の主張は、上記のクエリの基数の見積もりは次のとおりであるということです。

  1. いつ C2 > = S1C1

C2S1 下限あり S1C1

  1. いつ C2S1C1

164.317C2S1 上限あり S1C1

テストしたすべての例を説明するわけではありませんが、いくつかの例を見てみましょう。最初のデータ準備のために:

C1 = 1000

C2 = 2

S1 = 1.0

したがって、基数の見積もりは次のようになります。

2 * 164.317 = 328.634

以下の偽造不可能スクリーンショットはこれを証明しています:

example 1

文書化されていないトレースフラグ2363を使用して、何が起こっているかについていくつかの手がかりを得ることができます。

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB2].[dbo].[T1].SomeId

Loaded histogram for column QCOL: [SE_DB2].[dbo].[T1].SomeId from stats with id 2

Selectivity: 1

Stats collection generated: 

  CStCollFilter(ID=3, CARD=1000)

      CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

End selectivity computation

Begin selectivity computation

Input tree:

...

Plan for computation:

  CSelCalcSimpleJoinWithUpperBound (Using base cardinality)

      CSelCalcOneSided (RIGHT)

          CSelCalcCombineFilters_ExponentialBackoff (AND)

              CSelCalcFixedFilter (0.3)

              CSelCalcFixedFilter (0.3)

Selectivity: 0.164317

Stats collection generated: 

  CStCollJoin(ID=4, CARD=328.634 x_jtInner)

      CStCollFilter(ID=3, CARD=1000)

          CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

      CStCollBaseTable(ID=2, CARD=2 TBL: T2)

End selectivity computation

新しいCEでは、BETWEENの通常の16%の見積もりが得られます。これは、新しい2014 CEでの指数バックオフによるものです。各不等式の基数推定値は0.3であるため、BETWEENは0.3 * sqrt(0.3)= 0.164317として計算されます。 16%の選択性にT2とT1の行数を掛けると、推定値が得られます。十分に合理的なようです。 T2の行数を7に増やします。次のようになります。

C1 = 1000

C2 = 7

S1 = 1.0

したがって、以下の理由により、カーディナリティーの見積もりは1000になります。

7 * 164.317 = 1150> 1000

クエリプランはそれを確認します:

example 1

TF 2363をもう一度試すこともできますが、上限を尊重するために選択性が舞台裏で調整されたようです。 CSelCalcSimpleJoinWithUpperBoundにより、カーディナリティの推定値が1000を超えないようになっていると思います。

Loaded histogram for column QCOL: [SE_DB2].[dbo].[T1].SomeId from stats with id 2

Selectivity: 1

Stats collection generated: 

  CStCollFilter(ID=3, CARD=1000)

      CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

End selectivity computation

Begin selectivity computation

Input tree:

...

Plan for computation:

  CSelCalcSimpleJoinWithUpperBound (Using base cardinality)

      CSelCalcOneSided (RIGHT)

          CSelCalcCombineFilters_ExponentialBackoff (AND)

              CSelCalcFixedFilter (0.3)

              CSelCalcFixedFilter (0.3)

Selectivity: 0.142857

Stats collection generated: 

  CStCollJoin(ID=4, CARD=1000 x_jtInner)

      CStCollFilter(ID=3, CARD=1000)

          CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

      CStCollBaseTable(ID=2, CARD=7 TBL: T2)

T2を50000行にバンプしましょう。今私たちは持っています:

C1 = 1000

C2 = 50000

S1 = 1.0

したがって、基数の見積もりは次のようになります。

50000 * 1.0 = 50000

クエリプランはそれを再度確認します。すでに式を理解しておくと、見積もりを推測するのがはるかに簡単になります。

example 2

TF出力:

Loaded histogram for column QCOL: [SE_DB2].[dbo].[T1].SomeId from stats with id 2

Selectivity: 1

Stats collection generated: 

  CStCollFilter(ID=3, CARD=1000)

      CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

...

Plan for computation:

  CSelCalcSimpleJoinWithUpperBound (Using base cardinality)

      CSelCalcOneSided (RIGHT)

          CSelCalcCombineFilters_ExponentialBackoff (AND)

              CSelCalcFixedFilter (0.3)

              CSelCalcFixedFilter (0.3)

Selectivity: 0.001

Stats collection generated: 

  CStCollJoin(ID=4, CARD=50000 x_jtInner)

      CStCollFilter(ID=3, CARD=1000)

          CStCollBaseTable(ID=1, CARD=1000 TBL: T1)

      CStCollBaseTable(ID=2, CARD=50000 TBL: T2)

この例では、指数バックオフは無関係と思われます。

5000 * 1000 * 0.001 = 50000。

次に、SomeId値が0のT1に3k行を追加します。そのためのコードを記述します。

INSERT INTO T1 WITH (TABLOCK)
SELECT TOP 3000 NULL, NULL, 0
FROM   master..spt_values v1,
       master..spt_values v2;

UPDATE STATISTICS T1 WITH FULLSCAN;

今私たちは持っています:

C1 = 4000

C2 = 50000

S1 = 0.25

したがって、基数の見積もりは次のようになります。

50000 * 0.25 = 12500

クエリプランはそれを確認します:

example 3

これは、質問で述べたのと同じ動作です。テーブルに無関係な行を追加すると、カーディナリティの推定値が減少しました。なぜそれが起こったのですか?太字の行に注意してください。

Loaded histogram for column QCOL: [SE_DB2].[dbo].[T1].SomeId from stats with id 2

選択性:0.25

Stats collection generated: 

  CStCollFilter(ID=3, CARD=1000)

      CStCollBaseTable(ID=1, CARD=4000 TBL: T1)

End selectivity computation

Begin selectivity computation

Input tree:

...

Plan for computation:

  CSelCalcSimpleJoinWithUpperBound (Using base cardinality)

      CSelCalcOneSided (RIGHT)

          CSelCalcCombineFilters_ExponentialBackoff (AND)

              CSelCalcFixedFilter (0.3)

              CSelCalcFixedFilter (0.3)

選択性:0.00025

Stats collection generated: 

  CStCollJoin(ID=4, CARD=12500 x_jtInner)

      CStCollFilter(ID=3, CARD=1000)

          CStCollBaseTable(ID=1, CARD=4000 TBL: T1)

      CStCollBaseTable(ID=2, CARD=50000 TBL: T2)

End selectivity computation

この場合のカーディナリティ推定は次のように計算されたようです。

C1S1C2S1 /( S1C1

または、この特定の例の場合:

4000 * 0.25 * 50000 * 0.25 /(0.25 * 4000)= 12500

一般式はもちろん次のように簡略化できます。

C2S1

これは私が上で主張した式です。キャンセルすべきでないキャンセルがいくつかあるようです。 T1の行の総数が見積もりに関連すると予想します。

さらにT1に行を挿入すると、実際の下限がわかります。

INSERT INTO T1 WITH (TABLOCK)
SELECT TOP 997000 NULL, NULL, 0
FROM   master..spt_values v1,
       master..spt_values v2;

UPDATE STATISTICS T1 WITH FULLSCAN;

この場合の推定基数は1000行です。クエリプランとTF 2363出力は省略します。

最後に、この動作はかなり疑わしいですが、バグであるかどうかを宣言するのに十分な知識はありません。私の例はあなたの再現と正確には一致しませんが、私は同じ一般的な行動を観察したと信じています。また、最初のデータをどのように選択したかについては、少し幸運だと思います。オプティマイザーによるかなりの量の推測が行われているようですので、元のクエリが正確に見積もりに一致する100万行を返したという事実にあまり夢中になりません。

6
Joe Obbish