web-dev-qa-db-ja.com

TOP 1を追加するとパフォーマンスが劇的に低下するのはなぜですか?

かなり単純なクエリがあります

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

それは私に恐ろしいパフォーマンスを与えています(それが終わるのを待つことを決して気にしないような)。クエリプランは次のようになります。

enter image description here

ただし、TOP 1を削除すると、次のようなプランが表示され、1〜2秒で実行されます。

enter image description here

以下の正しいPKとインデックス付け。

TOP 1がクエリプランを変更したことは、私を驚かせませんでした。それが、それによって事態が大幅に悪化したことに少し驚いています。

注:この post の結果を読み、Row Goalなどの概念を理解しました。私が興味を持っているのは、使用するようにクエリを変更する方法です。より良い計画。現在、私はデータを一時テーブルにダンプし、それから最初の行を取り出しています。より良い方法があるかどうか私は思っています。

Edit事実の後でこれを読んでいる人のために、ここにいくつかの追加情報があります。

  • Document_Queue-PK/CIはD_IDであり、約5,000行があります。
  • Correspondence_Journal-PK/CIはFILE_NUMBER、CORRESPONDENCE_IDで、約1.4 milの行があります。

私が始めたとき、他のインデックスはありませんでした。 Correspondence_Journal(Document_Id、File_Number)で1つになりました

39
Kenneth Fisher

ハッシュ 結合を強制してみてください*

_SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
_

オプティマイザーはおそらくループがトップ1でより良くなるだろうと考えました、そしてそのようなことは理にかなっていますが、実際にはここでは機能しませんでした。ここでは推測ですが、おそらくそのスプールの推定コストはオフでした-TEMPDBを使用しています-TEMPDBのパフォーマンスが低い可能性があります。


* join hints に注意してください。プランテーブルのアクセス順序がクエリ内のテーブルのwritten順序と一致するように強制するためです。 (OPTION (FORCE ORDER)が指定されたかのように)。ドキュメントのリンクから:

BOL extract

これは、例では望ましくない影響を生成しない可能性がありますが、一般的には非常に良い可能性があります。 _FORCE ORDER_(暗黙的または明示的)は、very強力なヒントであり、強制的な順序を超えています。部分的な集計や並べ替えなど、幅広いオプティマイザー手法が適用されるのを防ぎます。

OPTION (HASH JOIN)queryヒントは、_FORCE ORDER_を意味するものではないため、適切なケースではそれほど煩わしくないかもしれません。ただし、クエリのすべての結合に適用されます。他のソリューションが利用可能です。

28
paparazzo

_ORDER BY_を使用して正しい計画を取得したので、独自のTOP演算子をロールバックすることもできますか?

_SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;
_

私の考えでは、上記のROW_NUMBER()のクエリプランは、_ORDER BY_がある場合と同じでなければなりません。クエリプランには、セグメント、シーケンスプロジェクト、最後にフィルターオペレーターが含まれているはずです。残りは、適切なプランのように見えるはずです。

30

編集:+1はこの状況で機能します。これは、_FILE_NUMBER_が整数のゼロでパディングされた文字列バージョンであることが判明したためです。文字列のここでのより良い解決策は、_''_(空の文字列)、値を追加すると順序に影響する可能性があるため、または数値が定数であるがsign(Rand()+1)などの非決定的関数を含むものを追加する可能性があるため、「ソートの解除」の考え方は依然として有効ですここでは、私の方法が理想的ではなかったというだけです。

+1

いいえ、私は何かに同意しているわけではありません。それは解決策としてです。クエリを_ORDER BY cj.FILE_NUMBER + 1_に変更すると、_TOP 1_の動作が異なります。

順序付けられたクエリに対して小さな行の目標が設定されていると、並べ替え演算子が存在しないようにするために、システムはデータを順番に消費しようとします。また、ハッシュテーブルの作成も回避され、最初の行を見つけるために多くの作業を行う必要がないことがわかります。あなたの場合、これは間違っています-それらの矢印の太さから、単一の一致を見つけるには大量のデータを消費する必要があるようです。

これらの矢印の太さは、_DOCUMENT_QUEUE_(DQ)テーブルが_CORRESPONDENCE_JOURNAL_(CJ)テーブルよりもはるかに小さいことを示しています。そして、最良の計画は、実際にはCJ行が見つかるまでDQ行をチェックすることです。実際、この厄介な_ORDER BY_がそこにない場合、クエリオプティマイザー(QO)はこれを実行します。これは、CJのカバーインデックスによって適切にサポートされます。

したがって、_ORDER BY_を完全に削除した場合、ネストされたループを含む計画を取得し、DQの行を反復処理して、CJにシークして行が存在することを確認すると思います。そして、_TOP 1_を使用すると、これは単一の行がプルされた後に停止します。

しかし、実際に_FILE_NUMBER_順の最初の行が必要な場合は、_ORDER BY CJ.FILE_NUMBER+1_を実行することで、システムをだまして(誤って)非常に役立つと思われるインデックスを無視させることができます。以前と同じ順序ですが、QOはそうではありません。 QOは全体の設定に重点を置くため、上位N並べ替え演算子を満足させることができます。このメソッドは、順序付けの値を計算するためのCompute Scalar演算子と、最初の行を取得するためのTop N Sort演算子を含むプランを生成する必要があります。しかし、これらの右側には、CJで多くのSeeksを実行する、Nice Nested Loopが表示されます。また、DQの何にも一致しない行の大きなテーブルを実行するよりもパフォーマンスが優れています。

ハッシュマッチは必ずしもひどいわけではありませんが、DQから返される行のセットがCJよりもはるかに小さい場合(予想どおり)、ハッシュマッチはより多くのCJをスキャンします。必要以上に。

注:クエリオプティマイザーは+0は何も変更しないと認識する可能性が高いため、+ 0ではなく+1を使用しました。もちろん、同じことが+1にも当てはまるかもしれませんが、現在ではない場合でも、将来のある時点で当てはまります。

29
Rob Farley

私はこの投稿の結果を読み、行の目標などの概念を理解しました。私が気になるのは、より良い計画を使用するようにクエリを変更する方法です。

OPTION (QUERYTRACEON 4138) を追加すると、最終的な計画について過度に規定せずに、そのクエリのみの行目標の効果がオフになり、おそらく最も簡単で直接的な方法になります。

このヒントを追加すると、アクセス許可エラーが発生する場合(DBCC TRACEON)、プランガイドを使用して適用できます。

プランガイドでのQUERYTRACEONの使用spaghettidba による

...または単にストアドプロシージャを使用します。

QUERYTRACEONに必要な権限は何ですか? by Kendra Little

7
Martin Smith

SQL Serverの新しいバージョンでは、オプティマイザが行の目標の最適化を適用できる場合に、次善のパフォーマンスを得るクエリを処理するためのさまざまな(間違いなく優れた)オプションが提供されています。 SQL Server 2016 SP1はDISABLE_OPTIMIZER_ROWGOAL USE HINTを導入しました。これはトレースフラグ4138と同じ効果があります。そのバージョンを使用していない場合は、OPTIMIZE FORクエリヒントを使用して返すように設計されたクエリプランを取得することもできます。以下のクエリは、質問の結果と同じ結果を返しますが、1行だけを取得するという目的では作成されません。

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
3
Joe Obbish

TOP(1)を実行しているので、ORDER BYを最初に決定論的にすることをお勧めします。少なくとも、これは結果が機能的に予測可能であることを保証します(常に回帰テストに役立ちます)。 DC.D_IDCJ.CORRESPONDENCE_IDを追加する必要があるようです。

クエリプランを見ると、クエリを簡略化することが役立つ場合があります。関連するすべてのDC行を一時テーブルに事前に選択して、QUEUE_DATEPRINT_LOCATIONのカーディナリティの推定に関する問題を排除することができます。行数が少ない場合、これは高速です。その後、永続テーブルを変更せずに、必要に応じてこの一時テーブルにインデックスを追加できます。

2
Simon Birch