以下は、私が本番環境で遭遇したものの単純化されたバージョンです(計画が異常に多数のバッチが処理された日に壊滅的に悪化しました)。
再現は、新しいカーディナリティエスティメータで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行が含まれます。
FromDate
、ToDate
、およびBatchNumber
は、すべて同じです。異なる唯一の値はSomeId
で、値は0
と999
の間です。
+------------+------------+--------+-----------+
| 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秒かかります。実際の行と推定される行は、計画内のすべてのオペレーターに最適です。
次に、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に減少しました(②)。
だから問題は...
BatchNumber <> 1
を使用して行を追加すると、BatchNumber = 1
?
テーブルに行を追加すると、最終的にはクエリ全体の推定行数が減るので、直感に反するようです。
クエリやテーブル内のデータを変更しても、一貫性は保証されないことに注意してください。クエリオプティマイザーは、カーディナリティ推定の異なる方法(ヒストグラムではなく密度を使用するなど)を使用して、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
フィルターの選択性
私の主張は、上記のクエリの基数の見積もりは次のとおりであるということです。
C2
> = S1
* C1
:
C2
*S1
下限ありS1
*C1
C2
< S1
* C1
:
164.317
*C2
*S1
上限ありS1
*C1
テストしたすべての例を説明するわけではありませんが、いくつかの例を見てみましょう。最初のデータ準備のために:
C1
= 1000
C2
= 2
S1
= 1.0
したがって、基数の見積もりは次のようになります。
2 * 164.317 = 328.634
以下の偽造不可能スクリーンショットはこれを証明しています:
文書化されていないトレースフラグ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
クエリプランはそれを確認します:
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
クエリプランはそれを再度確認します。すでに式を理解しておくと、見積もりを推測するのがはるかに簡単になります。
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
クエリプランはそれを確認します:
これは、質問で述べたのと同じ動作です。テーブルに無関係な行を追加すると、カーディナリティの推定値が減少しました。なぜそれが起こったのですか?太字の行に注意してください。
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
この場合のカーディナリティ推定は次のように計算されたようです。
C1
* S1
* C2
* S1
/( S1
* C1
)
または、この特定の例の場合:
4000 * 0.25 * 50000 * 0.25 /(0.25 * 4000)= 12500
一般式はもちろん次のように簡略化できます。
C2
* S1
これは私が上で主張した式です。キャンセルすべきでないキャンセルがいくつかあるようです。 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万行を返したという事実にあまり夢中になりません。