次の2つの式がまったく同じ結果をもたらすことを証明するのは非常に簡単です:今月の初日。
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
また、計算にはほぼ同じ時間がかかります。
SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); GO 1000000 GO SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); GO 1000000 SELECT SYSDATETIME();
私のシステムでは、両方のバッチが完了するまでに約175秒かかりました。
では、なぜ一方の方法をもう一方の方法よりも好むのでしょうか。 そのうちの1つがカーディナリティの推定を実際に台無しにした場合 。
簡単な入門書として、次の2つの値を比較してみましょう。
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), -- today: 2013-09-01 DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); -- today: 1786-05-01 --------------------------------------^^^^^^^^^^^^ notice how these are swapped
(ここに表示される実際の値は、この投稿を読んでいる時期によって変わることに注意してください。コメントで参照されている「今日」は、この投稿が書かれた日である2013年9月5日です。たとえば、2013年10月の出力は次のようになります。 2013-10-01
である および1786-04-01
。)
それが邪魔にならないように、私が何を意味するのかをお見せしましょう…
再現
クラスター化されたDATE
のみを使用して、非常に単純なテーブルを作成しましょう。 列を作成し、値が1786-05-01
の15,000行をロードします。 値が2013-09-01
の50行 :
CREATE TABLE dbo.DateTest ( CreateDate DATE ); CREATE CLUSTERED INDEX x ON dbo.DateTest(CreateDate); INSERT dbo.DateTest(CreateDate) SELECT TOP (15000) DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 UNION ALL SELECT TOP (50) DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) FROM sys.all_objects;
次に、これら2つのクエリの実際の計画を見てみましょう。
SELECT /* Query 1 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* Query 2 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);
グラフィカルな計画は正しく見えます:
DATEDIFF(MONTH、0、GETDATE())のグラフィカルプランクエリ
DATEDIFF(MONTH、GETDATE()、0)のグラフィカルプランクエリ
ただし、推定コストは手に負えないものです。15,000行を返す2番目のクエリと比較して、50行しか返さない最初のクエリの推定コストがどれほど高いかに注意してください。
また、[トップオペレーション]タブには、最初のクエリ(2013-09-01
を検索)が表示されます。 )実際には50行しか検出されなかったのに、15,000行が検出されると推定されました。 2番目のクエリは反対を示しています。1786-05-01
に一致する50行が見つかると予想されます。 、しかし15,000が見つかりました。このような誤ったカーディナリティの見積もりに基づいて、これがはるかに大きなデータセットに対するより複雑なクエリにどのような劇的な影響を与える可能性があるかを想像できると確信しています。
最初のクエリの[トップ操作]タブ[DATEDIFF(MONTH、0、 GETDATE())]
2番目のクエリの[トップ操作]タブ[DATEDIFF(MONTH、0、 GETDATE())]
月の初めを計算するために異なる式を使用する(投稿の冒頭でほのめかされている)クエリのわずかに異なるバリエーションでは、この症状は見られません:
SELECT /* Query 3 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
計画は上記のクエリ1と非常によく似ており、よく見ていない場合は、これらの計画は同等であると思います。
非DATEDIFFクエリのグラフィカルプラン
ただし、ここで[トップオペレーション]タブを見ると、見積もりが非常に優れていることがわかります。
この特定のデータサイズとクエリでは、正味のパフォーマンスへの影響(特に期間と読み取り)はほとんど関係ありません。また、クエリ自体は依然として正しいデータを返すことに注意することが重要です。見積もりが間違っているだけです(そして、ここで示したよりも悪い計画につながる可能性があります)。とはいえ、DATEDIFFを使用して定数を導出する場合 この方法でクエリ内で、実際に環境でこの影響をテストする必要があります。
では、なぜこれが発生するのですか?
簡単に言うと、SQLServerにはDATEDIFF
があります カーディナリティ推定の式を評価するときに2番目と3番目の引数を交換するバグ。これには、少なくとも周辺での定数畳み込みが含まれるようです。このBooksOnlineの記事には、定数畳み込みに関する詳細がたくさんありますが、残念ながら、この記事にはこの特定のバグに関する情報は含まれていません。
修正がありますか?それともありますか?
問題に対処すると主張するナレッジベースの記事(KB#2481274)がありますが、それ自体にいくつかの問題があります。
- KBの記事によると、この問題はSQL Server 2005、2008、および2008R2のさまざまなサービスパックまたは累積的な更新で修正されています。ただし、この症状は、記事が公開されてからさらに多くのCUが表示されているにもかかわらず、明示的に言及されていないブランチにまだ存在しています。この問題は、SQL Server 2008 SP3 CU#8(10.0.5828)およびSQL Server 2012 SP1 CU#5(11.0.3373)でも再現できます。
- 修正の恩恵を受けるには、トレースフラグ4199をオンにする必要があることを言及していません(特定のトレースフラグがオプティマイザに影響を与える可能性がある他のすべての方法から「恩恵」を受けます)。このトレースフラグが修正に必要であるという事実は、関連するConnectアイテム#630583に記載されていますが、この情報はKBの記事に戻っていません。 KBの記事もConnectアイテムも、原因(
DATEDIFF
への引数)についての洞察を提供していません。 評価中に交換されました)。プラス面として、トレースフラグをオンにして上記のクエリを実行します(OPTION (QUERYTRACEON 4199)
を使用) )誤った見積もりの問題がないプランを生成します。
- この問題を回避するには、動的SQLを使用することをお勧めします。私のテストでは、別の式(
DATEDIFF
を使用しない上記の式など)を使用します )SQLServer2008とSQLServer2012の両方の最新ビルドの問題を克服しました。ここで動的SQLを推奨することは、別の式で問題を解決できることを考えると、不必要に複雑で、おそらくやり過ぎです。ただし、動的SQLを使用する場合は、KB記事で推奨されている方法ではなく、この方法で使用します。最も重要なのは、SQLインジェクションのリスクを最小限に抑えることです。DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
(そして、
OPTION (RECOMPILE)
を追加できます そこで、SQL Serverがパラメータスニッフィングをどのように処理するかによって異なります。)これにより、
DATEDIFF
を使用しない以前のクエリと同じプランになります。 、適切な見積もりとクラスター化されたインデックスシークのコストの99.1%。あなたを誘惑する可能性のある別のアプローチ(そしてあなたが最初に調査を始めたとき、私はあなたを意味します)は、変数を使用して事前に値を計算することです:
DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;
このアプローチの問題は、変数を使用すると、安定した計画が得られることですが、カーディナリティは推測に基づくことになります(推測のタイプは統計の有無によって異なります) 。この場合、推定値と実際の値は次のとおりです。
変数を使用するクエリの[トップ操作]タブ これは明らかに正しくありません。 SQL Serverは、変数がテーブルの行の50%に一致すると推測したようです。
SQL Server 2014
SQL Server 2014で少し異なる問題を見つけました。最初の2つのクエリは修正されています(カーディナリティ推定量の変更またはその他の修正によって)。つまり、DATEDIFF
引数は切り替えられなくなりました。わーい!
ただし、別の式を使用する回避策に回帰が導入されたようです。現在、推定が不正確になっています(変数を使用する場合と同じ50%の推測に基づく)。これらは私が実行したクエリです:
SELECT /* 0, GETDATE() (2013) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* GETDATE(), 0 (1786) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); SELECT /* Non-DATEDIFF */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE())); DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); SELECT /* Variable */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d; DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT /* Dynamic SQL */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
推定コストと実際の実行時メトリックを比較するステートメントグリッドは次のとおりです。
SQLServer2014での5つの標本クエリの推定コスト
そして、これらは推定および実際の行数です(Photoshopを使用して組み立てられています):
SQLServer2014での5つのクエリの推定行数と実際の行数
この出力から、以前は問題を解決していた式が別の式を導入していることが明らかです。これがCTPで実行されている症状(たとえば、修正されるもの)なのか、それとも本当にリグレッションなのかはわかりません。
この場合、トレースフラグ4199(それ自体)は効果がありません。新しいカーディナリティ推定器は推測を行っており、単に正しくありません。それが実際のパフォーマンスの問題につながるかどうかは、この投稿の範囲を超えた他の多くの要因に大きく依存します。
この問題が発生した場合は、少なくとも現在のCTPでは、OPTION (QUERYTRACEON 9481, QUERYTRACEON 4199)
を使用して古い動作を復元できます。 。トレースフラグ9481は、これらのリリースノートで説明されているように、新しいカーディナリティ推定器を無効にします(これは確実に消えるか、少なくともある時点で移動します)。これにより、DATEDIFF
以外の正しい推定値が復元されます。 クエリのバージョンですが、残念ながら、変数に基づいて推測が行われる問題は解決されません(TF4199なしでTF9481のみを使用すると、最初の2つのクエリが古い引数交換動作に回帰します)。
結論
これは私にとって大きな驚きだったことを認めます。これは現実の問題であり、想像上の問題ではないことを私に忍耐強く説得してくれたMartinSmithとt-clausen.dkに敬意を表します。また、私が正気を保つのを助け、私が言うべきではないことを思い出させてくれたPaul White(@SQL_Kiwi)にも大いに感謝します。 :-)
このバグに気づかなかったので、特定の変更によるものではなく、クエリテキストを変更するだけでより良いクエリプランが生成されることを断固として主張しました。結局のところ、想定するクエリへの変更が発生することがあります。 違いはありません、実際にはそうなります。したがって、環境に同様のクエリパターンがある場合は、それらをテストして、カーディナリティの見積もりが正しく行われていることを確認することをお勧めします。そして、アップグレードするときにそれらを再度テストするためにメモを取ります。