web-dev-qa-db-ja.com

データの事前ステージングにより、実行計画のコストが急増する

調整しようとしている問題のあるクエリがあります。私たちの最初の考えの1つは、より大きな実行プランの一部を取り、それらの結果を中間の一時テーブルに格納してから、他の操作を実行することでした。

私が観察しているのは、データを一時テーブルに事前にステージングすると、実行計画のコストが上限を超える(22-> 1.1k)ことです。現在、これには計画を並列化できるという利点があります。これにより、実行時間が20%削減されましたが、この場合は、実行ごとのCPU使用率がはるかに高くなるに値しません。

レガシーCEがオンになっているSQL Server 2016 SP2を使用しています。

元の計画(コスト〜20):

Low cost plan

https://www.brentozar.com/pastetheplan/?id=ry-QGnkCM

元のSQL:

_WITH Object1(Column1, Column2, Column3, Column4, Column5, Column6)
AS
(
    SELECT  Object2.Column1, 
            Object2.Column2, 
            Object3.Column3, 
            Object3.Column4, 
            Object3.Column5, 
            Object3.Column6
    FROM Object4 AS Object5
    INNER JOIN Object6 AS Object2 ON Object2.Column2 = Object5.Column2 AND Object2.Column7 = 0
    INNER JOIN Object7 AS Object8 ON Object8.Column8 = Object2.Column9 AND Object8.Column7 = 0
    INNER JOIN Object9 AS Object3 ON Object3.Column10 = Object8.Column11 AND Object3.Column7 = 0
    INNER JOIN Object10 AS Object11 ON Object2.Column1 = Object11.Column1
    WHERE   Object8.Column12 IS NULL AND
            Object8.Column13 = Object5.Column13 AND 
            Object3.Column3 = Object5.Column3 AND 
            Object11.Column14 = Variable1
)
insert Object12
SELECT  Object13.Column2,
        Object13.Column3,
        MIN(Object13.Column4) AS Column15,
        MAX(Object13.Column4) AS Column16,
        COUNT(DISTINCT (CASE WHEN Object13.Column5 = 1 THEN Object13.Column1 END)) AS Column17,
        COUNT(DISTINCT (CASE WHEN Object13.Column6 = 0 THEN Object13.Column1 END)) AS Column18,
        COUNT(DISTINCT Object13.Column1) AS Column19
FROM Object1 AS Object13
GROUP BY Object13.Column2, Object13.Column3 OPTION (RECOMPILE) 
_

新しい計画(上の青色でハイライトされた領域が一時テーブルに事前にステージングされています-コスト〜1.1k):

enter image description here

https://www.brentozar.com/pastetheplan/?id=rycqG3JRf

新しいSQL:

_SELECT  Object1.Column1,
        Object1.Column2,
        MIN(Object2.Column3) AS Column4,
        MAX(Object2.Column3) AS Column5,
        COUNT(DISTINCT (CASE WHEN Object2.Column6 = 1 THEN Object1.Column7 END)) AS Column8,
        COUNT(DISTINCT (CASE WHEN Object2.Column9 = 0 THEN Object1.Column7 END)) AS Column10,
        COUNT(DISTINCT Object1.Column7) AS Column11
from Object3 Object1
join Object4 Object2 on Object2.Column12 = Object1.Column13 and Object2.Column2 = Object1.Column2
where Object2.Column14 = 0
GROUP BY Object1.Column1, Object1.Column2 OPTION (RECOMPILE) 
_

新しい計画のコストがこれほど高くなるのはなぜでしょうか。必要に応じて、下のテーブル/インデックスに関する追加情報を提供させていただきます。

元のプランの場合、selectではなくinsertを実行していることがわかります。それでも、selectはthatより高くつくべきではありません。

これが実際の実行計画です。計画コストが非常に高いため、それは並行して行われるため、これは懸念事項です。したがって、より高いCPUを使用します。また、データの事前ステージングなどの理由でプランのコストがそれほど高くなる理由についても興味があります。これにより、通常、元のコストよりも優れているとは言えません。

一時テーブルは、2番目のクエリでObject1.Column13およびObject1.Column2の複合クラスターPKとしてインデックス付けされます。これは、Object4の列(および順序)と一致します。 MAXDOPヒントを追加することはオプションですが、これは「なぜ世界中でそれほどコストがかかるのか」という学術的な演習でもありますか?

2番目のクエリにOPTION (ORDER GROUP)を追加しても、演算子/コストは変わりません。

注:

  • 最初のクエリのObject9は、2番目のクエリのObject4と同じオブジェクトです。
5

コストは、「実際の計画」においても、見積もりに基づいています。 2つのクエリプランを並べて比較することはできず、それらの1つは、オペレーターまたは合計プランのコストのみに基づいて実行するために、より多くのCPUを必要とすると結論付けることはできません。 1秒で実行される数百万のコストのクエリを作成できます。また、実行に効果的に永遠にかかる小さなコストのクエリを作成することもできます。あなたの場合、ハッシュ結合後のカーディナリティの見積もりが悪いため、最初のクエリのコストは22オプティマイザユニットだけです。

enter image description here

赤字の演算子は数百万回実行されますが、クエリオプティマイザーは数千回しか実行されないと想定しています。見積もりに基づくコストは、その作業を反映しません。青色の演算子は、カーディナリティエスティメータが単一の行を挿入することを期待しているテーブルスプールです。代わりに数百万を挿入します。その結果、黒で示された演算子(他のいくつかは表示されていません)は非効率的で、tempdbに溢れています。

他の計画では、tempdbにかなりの数の行を配置します。その結果、カーディナリティの見積もりはより妥当になりますが、それでもまだ理想的ではありません。

enter image description here

クエリオプティマイザーは、さらに多くの行を処理する必要があると予想し、その結果、クエリプランのコストが高くなります。非常に一般的な経験則として、パフォーマンスの向上と推定の向上が見られる場合がありますが、必ずしも希望どおりに機能するとは限りません。一時テーブルを使用して計画を見ると、改善の余地がいくつかあります。

  1. 元のクエリから一時表に完全なCTEを読み込みます。複数の異なる集計を含むクエリは、最適化するのが難しい場合があります。場合によっては、すべてのデータがスプールに(tempdbに)ロードされ、一部の集計が個別にスプールに適用されるクエリプランが表示されます。私の経験では、すべての作業は常にシリアルゾーンで行われます。クエリ内のすべての結合を削除すると、その最適化は得られないと思います。集計は一時テーブルに適用されるだけです。これにより、ほぼ同じデータをtempdbに書き込む作業が節約され、計画全体が並列処理の対象になります。

  2. 一時テーブルをヒープとして定義し、TABLOCKを使用してそれに書き込みます。現在、クラスター化インデックスを使用しているようです。つまり、並列挿入の対象ではありません。

  3. これらの tricks のいずれかを使用して、クエリをバッチモードに適格にすることを検討してください。複数の異なるアグリゲートを使用すると、バッチモードのアグリゲートが大幅に効率的になります。

これらの手順をいくつか組み合わせることで、実行時間を大幅に改善できると思います。匿名化された計画は解釈が難しいため、一部を簡単に分析したことに注意してください。

6
Joe Obbish