web-dev-qa-db-ja.com

クエリが恐ろしい実行プランを選択するのはなぜですか?

アプリケーションが送信するクエリをより効率的に作成しようとしています。 SSMSでクエリをわずかに変更したところ、約1秒で実行されます。

クエリA

SELECT  O.Code AS 'Code', O.[Action] AS 'Action',
    SUM(OpenResponseWithin) AS 'OpenResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseWithinPercentage',
    SUM(OpenResponseAfter) AS 'OpenResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseAfterPercentage',
    (SUM(OpenResponseWithin)+SUM(OpenResponseAfter)) AS 'OpenTotal',
    SUM(CloseResponseWithin) AS 'CloseResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseWithinPercentage',
    SUM(CloseResponseAfter) AS 'CloseResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseAfterPercentage',
    SUM(CloseNever) AS 'CloseNever',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseNever))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseNeverPercentage'
FROM Custom_OpenCodeMRC O
WHERE O.ActionDate BETWEEN '10/5/2017' AND '12/4/2017'
      AND
      O.Code IN ('BERZ20','BERZ21','BERZ24','BERZ50','FTHZ63','YOR56','YOR57')
GROUP BY O.Code,O.[Action]
ORDER BY O.Code,O.[Action]

アプリケーションがパラメーターを使用してクエリを渡す方法をそのままにしておくと、実行に少なくとも20秒かかります。

クエリB

DECLARE @parm1 NVARCHAR(10) = '10/05/2017';
DECLARE @parm2 NVARCHAR(10) = '12/04/2017';
DECLARE @parm9 NVARCHAR(6) = 'BERZ20';
DECLARE @parm8 NVARCHAR(6) = 'BERZ21';
DECLARE @parm7 NVARCHAR(6) = 'BERZ24';
DECLARE @parm6 NVARCHAR(6) = 'BERZ50';
DECLARE @parm5 NVARCHAR(6) = 'FTHZ63';
DECLARE @parm4 NVARCHAR(5) = 'YOR56';
DECLARE @parm3 NVARCHAR(5) = 'YOR57';

SELECT O.Code AS 'Code',O.[Action] AS 'Action',
    SUM(OpenResponseWithin) AS 'OpenResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseWithinPercentage',
    SUM(OpenResponseAfter) AS 'OpenResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseAfterPercentage',
    (SUM(OpenResponseWithin)+SUM(OpenResponseAfter)) AS 'OpenTotal',
    SUM(CloseResponseWithin) AS 'CloseResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseWithinPercentage',
    SUM(CloseResponseAfter) AS 'CloseResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseAfterPercentage',
    SUM(CloseNever) AS 'CloseNever',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseNever))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseNeverPercentage'
FROM Custom_OpenCodeMRC O
WHERE O.ActionDate BETWEEN @parm1 AND @parm2
      AND
      O.Code IN (@parm3,@parm4,@parm5,@parm6,@parm7,@parm8,@parm9)
GROUP BY O.Code,O.[Action]
ORDER BY O.Code Asc,O.[Action] Asc

実行計画を保存しましたが、非常に長いです。誰かがそれらを見てみたい場合は、XMLで計画を投稿できます。ここにいくつかのクエリ統計があります。

 +-----------+---------------------+
 | Query A   | CPU time = 1439 ms  |
 | Query B   | CPU time = 23282 ms |
 +-----------+---------------------+

そして

+----------------+--------------+--------------+---------------+---------------+
|   Table        |  Query A     |   Query B    |  Query A      |  Query B      |
|                |  Scan Count  |  Scan Count  | Logical Reads | Logical Reads |
+----------------+--------------+--------------+---------------+---------------+
| ResponseAction |      1       |      1       |      2        |      2        |
|    Code        |      7       |      5       |      14       |      13       |
|   Workfile     |      0       |      0       |      0        |      0        |
|   Worktable    |      57      |    1,305     |    30,712     |    1,735,281  |
|   Response     |      7       |      7       |   12,1136     |    12,1137    |
| ResponseHistory|      24      |      23      |     3,507     |     3,507     |
|    Ticket      |      5       |      5       |     907       |      907      |
|  Organization  |      0       |      5       |    3,479      |     1,463     |
+----------------+--------------+--------------+---------------+---------------+

どちらもまったく同じ結果になりますが、実行時間は大きく異なります。クエリAがクエリBよりもはるかに効率的な実行プランを使用する理由を誰かに説明してもらえますか?ありがとう!

実行計画へのリンクは次のとおりです。

クエリA

クエリB

4
Jason

謎の場所への旅行を計画しなければならないことを想像してみてください。それは世界のどこにでもありえます。旅行のオプションは、徒歩、車、または飛行機です。あなたは目的地を知る前に選択する必要があります。何を選びますか?私は飛行機で旅行を選ぶでしょう。世界で可能なすべてのオプションの平均移動時間をとる場合、これは最良のオプションです。もちろん、目的地が通りだと言ってしまうと、運が悪くなる可能性があります。飛行機を利用することは、その目的地に対して比較的非効率的です。一方、それは確かに数千マイル歩く必要があるよりも優れたオプションです。

クエリでローカル変数を使用すると、クエリオプティマイザーが同じ種類の状況になることがよくあります。クエリオプティマイザーは、すべての可能な入力変数に対して適切に機能するキャッシュされたプランを作成することを目的としています。多くの場合、統計オブジェクトの密度を使用します。これは、「平均」カーディナリティ推定値を取得する1つの方法です。デフォルトでは、特定のパラメーターの値がクエリに埋め込まれず、それらの値を使用して効率的なプランが作成されます。

別の見方をすると、データとテーブルの構造は、1つのクエリプランは比較的少量のデータを処理するのに適していますが、別のクエリプランは比較的大量のデータを処理するのに適しています。たとえば、@parm1の値が'10/05/2017 'から'10/05/2000'にシフトした場合に、クエリプランを変更することができます。ローカル変数を使用すると、キャッシュされたプランが1つだけ得られます。そのキャッシュされたプランは、これらの日付値の1つまたは両方の最適なパフォーマンスよりも低くなります。

クエリBのパフォーマンスを改善する最も簡単な修正は、RECOMPILEヒントを追加することです。これにより、実行時の変数値に基づいてカスタムプランを提供する パラメータ埋め込み最適化 が有効になります。 RECOMPILEの欠点は、クエリオプティマイザーが毎回新しいクエリプランをコンパイルする必要があることです。優れたクエリの実行に1秒以上かかる場合、おそらくそれについてあまり心配する必要はありません。

18
Joe Obbish