2013年に、DATEDIFF()
の2番目と3番目の引数がオプティマイザーにあるバグについて書きました。 交換される可能性があります。これにより、行数の見積もりが不正確になり、実行プランの選択が不十分になる可能性があります。
- パフォーマンスの驚きと仮定:DATEDIFF
先週末、私は同じような状況を知り、それが同じ問題であるとすぐに思い込みました。結局のところ、症状はほぼ同じように見えました:
-
WHERE
に日付/時刻関数がありました 条項。- 今回は
DATEADD()
DATEDIFF()
の代わりに 。
- 今回は
- 実際の行数が300万を超えているのに比べて、明らかに誤った行数の見積もりが1でした。
- これは実際には0の見積もりでしたが、SQLServerは常にそのような見積もりを1に切り上げます。
- 見積もりが低いため、プランの選択が不十分でした(この場合はループ結合が選択されました)。
問題のあるパターンは次のようになりました:
WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());
ユーザーはいくつかのバリエーションを試しましたが、何も変わりませんでした。最終的に、述語を次のように変更することで、問題を回避することができました。
WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;
これにより、より適切な見積もりが得られました(通常の30%の不等式の推測)。正しくありません。ループ結合は排除されましたが、この述語には2つの大きな問題があります。
- そうではありません 同じクエリ。365日前の特定の時点よりも大きいのではなく、通過する365日の境界を探しているためです。統計学的に重要な?そうでないかもしれない。しかし、技術的には同じではありません。
- 列に対して関数を適用すると、式全体が引数不可になり、フルスキャンになります。テーブルに1年強のデータしか含まれていない場合、これは大したことではありませんが、テーブルが大きくなったり、述語が狭くなったりすると、これが問題になります。
繰り返しになりますが、DATEADD()
という結論に飛びつきました。 操作が問題であり、DATEADD()
に依存しないアプローチを推奨しました – datetime
を作成する 現在の時間のすべての部分から、DATEADD()
を使用せずに1年を引くことができます :
WHERE [column] >= DATETIMEFROMPARTS( DATEPART(YEAR, SYSUTCDATETIME())-1, DATEPART(MONTH, SYSUTCDATETIME()), DATEPART(DAY, SYSUTCDATETIME()), DATEPART(HOUR, SYSUTCDATETIME()), DATEPART(MINUTE, SYSUTCDATETIME()), DATEPART(SECOND, SYSUTCDATETIME()), 0);
かさばるだけでなく、これには独自の問題がいくつかありました。つまり、うるう年を適切に説明するために、一連のロジックを追加する必要があるということです。 1つ目は、2月29日に実行されても失敗しないようにするため、2つ目は、すべての場合に正確に365日を含めることです(飛躍日の翌年の366日ではありません)。もちろん簡単に修正できますが、ロジックがはるかに醜くなります。特に、中間変数や複数のステップが不可能なビュー内にクエリを存在させる必要があるためです。
その間に、OPは1行の見積もりに失望したConnectアイテムを提出しました:
- 接続#2567628:DateAdd()による制約が適切な見積もりを提供しない
次に、Paul White(@SQL_Kiwi)がやって来て、以前の多くの場合と同様に、問題にさらに光を当てました。彼は2011年にErlandSommarskogによって提出された関連するConnectアイテムを共有しました:
- 接続#685903:sysdatetimeがdateadd()式に表示される場合の推定が正しくありません
基本的に、問題は、SYSDATETIME()
の場合だけでなく、不十分な見積もりが行われる可能性があることです。 (またはSYSUTCDATETIME()
)は、Erlandが最初に報告したように表示されますが、datetime2
式は述語に含まれます(おそらくDATEADD()
の場合のみ も使用されます)。そして、それは双方向に進むことができます– >=
を交換する場合 <=
の場合 、見積もりはテーブル全体になるため、オプティマイザーはSYSDATETIME()
を調べているようです。 定数としての値であり、DATEADD()
などの操作を完全に無視します それに対して実行されます。
Paulは、回避策は単にdatetime
を使用することであると共有しました 適切なデータ型に変換する前に、日付を計算する場合と同等です。この場合、SYSUTCDATETIME()
を交換できます GETUTCDATE()
に変更します :
WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));
はい、これにより精度がわずかに低下しますが、
テーブルには過去1年間のデータがほぼ独占的に含まれているため、読み取りは類似しています。したがって、シークでさえ、ほとんどのテーブルの範囲スキャンになります。行数は同じではありません。これは、(a)2番目のクエリが深夜にカットオフし、(b)3番目のクエリに今年初めの飛躍日のために余分なデータが含まれているためです。いずれにせよ、これは、DATEADD()
を削除することで、適切な見積もりに近づく方法を示しています。 、ただし、適切な修正は、直接の組み合わせを削除することです。 DATEADD()
の およびdatetime2
。
見積もりがどのように間違っているかをさらに説明するために、元のクエリとPaulの書き直しに異なる引数と方向を渡すと、前者の見積もり行の数は常に現在の時刻に基づいていることがわかります。経過した日数によって変化しない(Paulは毎回比較的正確です):
最初のクエリの実際の行は、長い昼寝の後に実行されたため、わずかに低くなっています。
見積もりは必ずしもこれほど良いとは限りません。私のテーブルは比較的安定した分布になっています。これを自分で試してみたい場合に備えて、次のクエリを入力し、統計をフルスキャンで更新しました。
-- OP's table definition: CREATE TABLE dbo.DateaddRepro ( SessionId int IDENTITY(1, 1) NOT NULL PRIMARY KEY, CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME() ); GO CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc] ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId); GO INSERT dbo.DateaddRepro(CreatedUtc) SELECT dt FROM ( SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER() OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE()) FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2 ) AS x; UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN; SELECT DISTINCT SessionId FROM dbo.DateaddRepro WHERE /* pick your WHERE clause to test */;
新しいConnectアイテムについてコメントしましたが、戻ってStackExchangeの回答を修正する可能性があります。
物語の教訓
DATEADD()
の組み合わせは避けてください datetime2
を生成する式を使用 、特に古いバージョンのSQL Server(これはSQL Server 2012にありました)。また、SQL Server 2016でも、古いカーディナリティ推定モデルを使用している場合(互換性レベルが低いため、またはトレースフラグ9481を明示的に使用しているため)、問題が発生する可能性があります。このような問題は微妙で、すぐに明らかになるとは限らないので、これが思い出させるものになることを願っています(次回、同じようなシナリオに出くわしたときでも)。前回の投稿で提案したように、このようなクエリパターンがある場合は、正しい見積もりを取得していることを確認し、システムに大きな変更があった場合(アップグレードやサービスパックなど)にもう一度確認するためにどこかにメモを取ります。