クエリプランの作成コストを測定または検索する方法


18

パラメータスニッフィングにより、「悪い」実行プランがプランキャッシュに到達し、その後のストアドプロシージャの実行が非常に遅くなる典型的なケースがあります。ローカル変数でこの問題を「解決」できますが、OPTIMIZE FOR ... UNKNOWN、およびでOPTION(RECOMPILE)。ただし、クエリに飛び込んで最適化を試みることもできます。

私がすべきかどうかを判断しようとしています:問題を修正する時間が限られているので、それをしないことのコストを知りたいです。ご覧のとおり、単にに固執するOPTION(RECOMPILE)と、最終的な効果は、クエリが実行されるたびにクエリプランが再作成されることです。だから、私は知る必要があると思う:

クエリプランを作成するためのコストを調べる方法

私自身の質問に答えるために、(このクエリで)Googled を使い、dm_exec_query_statsDMVの列のドキュメントを 調べました。この情報を見つけるために、「実際のクエリプラン」のSSMSの出力ウィンドウも調べました。最後に、DBA.SE検索しました。それらのどれも答えを導きませんでした。

誰か教えてもらえますか?計画の作成に必要な時間を見つけたり測定したりすることは可能ですか?


5
Benjamin NevarezによるInside the SQL Server Query Optimizerのコピーを入手することをお勧めします。それは無料です。第5章「最適化プロセス」は、クエリのコンパイル時間の計算に役立つ場合があります。少なくとも、オプティマイザーがクエリプランを作成するために何を行うかについては有益です。
マークシンキンソン14

回答:


18

クエリプランを作成するためのコストを調べる方法

たとえば、クエリプランのルートノードのプロパティを確認できます。

ルートプロパティの抽出
(無料のSentry One Plan Explorerのスクリーンショット

この情報は、たとえば次の関係に基づいたクエリを使用して、プランキャッシュをクエリすることでも利用できます。

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

結果フラグメント

これらの種類のクエリを処理するためのオプションの完全な取り扱いについては、Erland Sommarskogの最近更新された記事を参照してください


4

「コスト」が時間の面であると仮定すると(;-の面で他に何ができるかはわかりませんが)、少なくとも次のようなことを行うことでそれを理解できるはずです。

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

[メッセージ]タブで報告される最初の項目は次のとおりです。

SQL Serverの解析およびコンパイル時間:

これを少なくとも10回実行し、「CPU」ミリ秒と「経過」ミリ秒の両方を平均します。

理想的には、これを実稼働環境で実行して、正確な時間の見積もりを取得できるようにしますが、実稼働環境でプランキャッシュをクリアできることはほとんどありません。幸いなことに、SQL Server 2008以降では、キャッシュから特定の計画をクリアできるようになりました。その場合、次のことができます。

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

ただし、「悪い」キャッシュプランの原因となるパラメーターに渡される値の変動性に応じて、OPTION(RECOMPILE)との間の中間点であると考える別の方法がありますOPTION(OPTIMIZE FOR UNKNOWN)ます:動的SQL。はい、私はそれを言った。また、パラメータ化されていない動的SQLを意味します。その理由は次のとおりです。

少なくとも1つ以上の入力パラメーター値に関して、明らかに不均一な分布のデータがあります。上記のオプションの欠点は次のとおりです。

  • OPTION(RECOMPILE)すべての実行計画を生成し、あなたが恩恵を受けることができることはありませんいかなる再び渡されたパラメータ値は、前の実行(S)と同じであっても、計画の再利用。頻繁に(数秒に1回またはそれ以上の頻度で)呼び出されるprocの場合、これは時折の恐ろしい状況からあなたを救いますが、それでも常にそれほど素晴らしい状況ではありません。

  • OPTION(OPTIMIZE FOR (@Param = value)) その特定の値に基づいて計画を生成します。これにより、いくつかのケースに役立つ可能性がありますが、それでも現在の問題を受け入れることができます。

  • OPTION(OPTIMIZE FOR UNKNOWN)平均分布の量に基づいて計画を生成します。これは、一部のクエリには役立ちますが、他のクエリには損害を与えます。これは、ローカル変数を使用するオプションと同じである必要があります。

ただし、動的SQLを正しく実行すると、渡されるさまざまな値に、理想的な(まあ、できる限り)独自のクエリプランを持たせることができます。ここでの主なコストは、渡される値の種類が増えると、キャッシュ内の実行計画の数が増え、メモリを占有することです。軽微な費用は次のとおりです。

  • SQLインジェクションを防ぐために文字列パラメーターを検証する必要がある

  • ダイナミックSQLには直接的なテーブル権限が必要なため、証明書と証明書ベースのユーザーをセットアップして理想的なセキュリティ抽象化を維持する必要があるかもしれません。

したがって、ここでは、1秒間に複数回呼び出され、それぞれが数百万行の複数のテーブルにヒットするprocがあったときに、この状況をどのように管理しているかを示します。私が試しましたOPTION(RECOMPILE)が、これはパラメータスニッフィング/不良キャッシュプランの問題を持たないケースの99%でプロセスに非常に有害であることが判明しました。また、これらのprocの1つには約15個のクエリがあり、そのうち3〜5個だけがここで説明したように動的SQLに変換されたことに留意してください。特定のクエリに必要でない限り、動的SQLは使用されませんでした。

  1. ストアドプロシージャへの入力パラメーターが複数ある場合、どのパラメーターが非常に異なるデータ分布を持つ列で使用され(したがって、この問題が発生する)、どのパラメーターがより均等な分布を持つ列で使用される(そうすべきではない)かを把握するこの問題の原因)。

  2. 均等に分散された列に関連付けられたproc入力パラメーターのパラメーターを使用して、動的SQL文字列を作成します。このパラメーター化により、このクエリに関連するキャッシュ内の実行計画の増加を削減できます。

  3. 非常に多様な分布に関連付けられている残りのパラメーターについては、それらをリテラル値として動的SQLに連結する必要があります。一意のクエリはクエリテキストへの変更によって決定されるため、持つことWHERE StatusID = 1は、持つこととは異なるクエリ、したがって異なるクエリプランWHERE StatusID = 2です。

  4. クエリのテキストに連結されるproc入力パラメーターのいずれかが文字列の場合、SQLインジェクションから保護するために検証する必要があります(ただし、渡される文字列がユーザーではなくアプリですが、まだ)。少なくともREPLACE(@Param, '''', '''''')、単一引用符がエスケープされた単一引用符になることを確認するために行います。

  5. 必要に応じて、ユーザーの作成に使用される証明書を作成し、ストアドプロシージャに署名して、直接テーブルアクセス許可が新しい証明書ベースのユーザーにのみ付与され、[public]そうでない場合はそのようなアクセス許可を持たないユーザーに付与されるようにします。

procの例:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;

回答に時間を割いてくださりありがとうございます!コンパイル時間を取得する方法についての最初のビットについては、@ PaulWhiteのアプローチを使用して得られる結果よりも3倍低いことを考えると、少し懐疑的です。-動的SQLビットの2番目は興味深いです(実装にも時間がかかりますが、少なくともクエリを平手打ちする以上のこともあります)。このsprocは統合テストでうまく利用されているので、あまり害はありませ。-いずれにせよ:あなたの洞察力に感謝します!OPTION
Jeroen 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.