先週、#BackToBasicsという投稿を公開しました:DATEFROMPARTS()
、ここでは、この2012+関数を使用して、よりクリーンで引数可能な日付範囲クエリを実行する方法を示しました。これを使用して、無制限の日付述語を使用し、関連する日付/時刻列にインデックスがある場合、インデックスの使用率が大幅に向上し、I / Oが低くなる(または最悪の場合)可能性があることを示しました。 、同じ、何らかの理由でシークを使用できない場合、または適切なインデックスが存在しない場合):
しかし、それは話の一部にすぎません(そして、明確にするために、DATEFROMPARTS()
シークを取得するために技術的に必要なわけではありません。その場合は、よりクリーンになります)。少しズームアウトすると、見積もりが正確とはほど遠いことがわかります。これは、前回の投稿では紹介したくなかった複雑さです。
これは、不等式述語と強制スキャンの両方で珍しいことではありません。そしてもちろん、私が提案した方法では、最も不正確な統計が得られるのではないでしょうか。基本的なアプローチは次のとおりです(以前の投稿からテーブルスキーマ、インデックス、サンプルデータを取得できます):
CREATE PROCEDURE dbo.MonthlyReport_Original @Year int, @Month int AS BEGIN SET NOCOUNT ON; DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1); DECLARE @End date = DATEADD(MONTH, 1, @Start); SELECT DateColumn FROM dbo.DateEntries WHERE DateColumn >= @Start AND DateColumn < @End; END GO
現在、不正確な見積もりが常に問題になるとは限りませんが、2つの極端な状況で非効率的なプランの選択に関する問題が発生する可能性があります。選択した範囲でテーブルまたはインデックスの割合が非常に小さいか非常に大きい場合、単一の計画は最適ではない可能性があります。これにより、SQLServerがデータ分散が不均一になる時期を予測するのが非常に困難になる可能性があります。ジョセフサックは、彼の投稿「実行計画の品質に対する10の一般的な脅威」で、悪い見積もりが影響を与える可能性のあるより典型的なことを概説しました。
「[…]不正な行の見積もりは、インデックスの選択、シークとスキャンの操作、並列とシリアルの実行、結合アルゴリズムの選択、内部と外部の物理的な結合の選択(ビルドとプローブなど)、スプールの生成など、さまざまな決定に影響を与える可能性があります。ブックマークルックアップと完全なクラスター化またはヒープテーブルアクセス、ストリームまたはハッシュ集計の選択、およびデータ変更でワイドプランとナロープランのどちらを使用するか。」
他にも、メモリの付与が大きすぎたり小さすぎたりするようなものがあります。彼は続けて、悪い見積もりのより一般的な原因のいくつかを説明しますが、この場合の主な原因は彼のリストから欠落しています:推測。ローカル変数を使用して着信int
を変更しているため 単一のローカルdate
へのパラメータ 変数の場合、SQL Serverは値が何であるかを認識しないため、テーブル全体に基づいてカーディナリティの標準化された推測を行います。
私が提案したアプローチの見積もりは5,170行であることが上記でわかりました。これで、不等式の述語があり、SQL Serverがパラメーター値を知らない場合、テーブルの30%が推測されることがわかりました。 31,645 * 0.3
5,170ではありません。 31,465 * 0.3 * 0.3
でもありません 、同じ列に対して実際に2つの述語が機能していることを思い出したとき。では、この5,170の値はどこから来るのでしょうか。
Paul Whiteが彼の投稿「複数の述語のカーディナリティ推定」で説明しているように、SQL Server 2014の新しいカーディナリティ推定器は指数バックオフを使用するため、テーブルの行数(31,465)に最初の述語の選択性(0.3)を掛けます。 、次にそれを平方根で乗算します 2番目の述語(〜0.547723)の選択性の評価。
31,645 *(0.3)* SQRT(0.3)〜=5,170.227これで、SQLServerが見積もりを出した場所を確認できます。それについて何かをするために使用できる方法のいくつかは何ですか?
- 日付パラメータを渡します。 可能であれば、個別の整数パラメーターではなく、適切な日付パラメーターを渡すようにアプリケーションを変更できます。
- ラッパープロシージャを使用します。 方法#1のバリエーション(たとえば、アプリケーションを変更できない場合)は、最初から作成された日付パラメーターを受け入れる2番目のストアドプロシージャを作成することです。
-
OPTION (RECOMPILE)
を使用する 。 クエリを実行するたびにわずかなコンパイルコストがかかりますが、これにより、SQL Serverは、不明、最初、または平均のパラメーター値に対して単一のプランを最適化するのではなく、毎回提示される値に基づいて最適化するようになります。 (このトピックの詳細な取り扱いについては、Paul Whiteの「パラメーターのスニッフィング、埋め込み、およびRECOMPILEオプション」を参照してください。
- 動的SQLを使用します。 動的SQLに構築された
date
を受け入れさせる 変数は適切なパラメーター化を強制します(date
を使用してストアドプロシージャを呼び出したかのように) パラメータ)、しかしそれは少し醜く、維持するのが難しいです。
- ヒントとトレースフラグを混乱させます。 ポールホワイトは、前述の投稿でこれらのいくつかについて話します。
これが完全なリストであることを示唆するつもりはありません。また、ヒントやトレースフラグに関するポールのアドバイスを繰り返すつもりはないので、最初の4つのアプローチが悪い見積もりで問題をどのように軽減できるかを示すことに焦点を当てます。 。
1。日付パラメータ
CREATE PROCEDURE dbo.MonthlyReport_TwoDates @Start date, @End date AS BEGIN SET NOCOUNT ON; SELECT /* Two Dates */ DateColumn FROM dbo.DateEntries WHERE DateColumn >= @Start AND DateColumn < @End; END GO
2。ラッパーの手順
CREATE PROCEDURE dbo.MonthlyReport_WrapperTarget @Start date, @End date AS BEGIN SET NOCOUNT ON; SELECT /* Wrapper */ DateColumn FROM dbo.DateEntries WHERE DateColumn >= @Start AND DateColumn < @End; END GO CREATE PROCEDURE dbo.MonthlyReport_WrapperSource @Year int, @Month int AS BEGIN SET NOCOUNT ON; DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1); DECLARE @End date = DATEADD(MONTH, 1, @Start); EXEC dbo.MonthlyReport_WrapperTarget @Start = @Start, @End = @End; END GO
3。オプション(再コンパイル)
CREATE PROCEDURE dbo.MonthlyReport_Recompile @Year int, @Month int AS BEGIN SET NOCOUNT ON; DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1); DECLARE @End date = DATEADD(MONTH, 1, @Start); SELECT /* Recompile */ DateColumn FROM dbo.DateEntries WHERE DateColumn >= @Start AND DateColumn < @End OPTION (RECOMPILE); END GO
4。動的SQL
CREATE PROCEDURE dbo.MonthlyReport_DynamicSQL @Year int, @Month int AS BEGIN SET NOCOUNT ON; DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1); DECLARE @End date = DATEADD(MONTH, 1, @Start); DECLARE @sql nvarchar(max) = N'SELECT /* Dynamic SQL */ DateColumn FROM dbo.DateEntries WHERE DateColumn >= @Start AND DateColumn < @End;'; EXEC sys.sp_executesql @sql, N'@Start date, @End date', @Start, @End; END GO
テスト
4セットの手順が整っているので、SQLServerが導き出した計画と見積もりを示すテストを簡単に作成できました。ある月は他の月より忙しいので、私は3つの異なる月を選び、それらをすべて複数回実行しました。
DECLARE @Year int = 2012, @Month int = 7; -- 385 rows DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1); DECLARE @End date = DATEADD(MONTH, 1, @Start); EXEC dbo.MonthlyReport_Original @Year = @Year, @Month = @Month; EXEC dbo.MonthlyReport_TwoDates @Start = @Start, @End = @End; EXEC dbo.MonthlyReport_WrapperSource @Year = @Year, @Month = @Month; EXEC dbo.MonthlyReport_Recompile @Year = @Year, @Month = @Month; EXEC dbo.MonthlyReport_DynamicSQL @Year = @Year, @Month = @Month; /* repeat for @Year = 2011, @Month = 9 -- 157 rows */ /* repeat for @Year = 2014, @Month = 4 -- 2,115 rows */
結果?すべてのプランで同じインデックスシークが生成されますが、推定値は3つの日付範囲すべてでのみ正しいです。 OPTION (RECOMPILE)
で バージョン。残りは、パラメータの最初のセット(2012年7月)から導出された推定値を引き続き使用するため、最初ののより良い推定値を取得します。 実行、その見積もりは、後続にとって必ずしも良いとは限りません。 さまざまなパラメータを使用した実行(パラメータスニッフィングの古典的な教科書の場合):
上記はSQLSentryPlan Explorerからの*正確な*出力ではないことに注意してください。たとえば、外部ストアドプロシージャの呼び出しとパラメータ宣言を示すステートメントツリーの行を削除しました。
毎回コンパイルする戦術が自分に最適かどうか、またはそもそも何かを「修正」する必要があるかどうかを判断するのはあなた次第です。ここでは、同じ計画が作成され、実行時のパフォーマンスメトリックに目立った違いはありませんでした。しかし、より大きなテーブル、より偏ったデータ分布、および述語値のより大きな分散(たとえば、1週間、1年、およびその間のすべてをカバーできるレポートを検討してください)では、調査する価値があるかもしれません。また、ここでメソッドを組み合わせることができることに注意してください。たとえば、適切な日付パラメータに切り替えて、OPTION (RECOMPILE)
を追加することができます。 、必要に応じて。
結論
意図的な単純化であるこの特定のケースでは、正しい見積もりを取得する努力は実際には成果を上げませんでした。別の計画を取得することはなく、実行時のパフォーマンスは同等でした。ただし、これが違いを生む場合は確かに他にもあります。推定の不一致を認識し、データが大きくなったり、分布が歪んだりするときに問題になる可能性があるかどうかを判断することが重要です。残念ながら、多くの変数がコンパイルオーバーヘッドが正当化されるかどうかに影響するため、白黒の答えはありません。多くのシナリオと同様に、ITDEPENDS™ …