このクエリの結果の列をすべて選択するのは、関心のある1つの列を選択するより速いのはなぜですか


13

を使用するselect *と、読み取りがはるかに少ないだけでなく、使用するよりも大幅に少ないCPU時間を使用するクエリがありますselect c.Foo

これはクエリです:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

これは、主に表Bにある2,473,658の論理読み取りで終了しました。26,562CPUを使用し、期間は7,965でした。

これは生成されたクエリプランです。

単一の列の値の選択から計画する PasteThePlan:https ://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

に変更c.IDする*と、クエリは107,049の論理読み取りで終了し、3つのテーブルすべてにほぼ均等に広がりました。4,266 CPUを使用し、期間は1,147でした。

これは生成されたクエリプランです。

すべての値の選択から計画する PasteThePlan:https ://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

私はこれらの結果と、ジョー・Obbishによって提案されたクエリヒントを使用しようとしました:
select c.IDヒントなし:https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.IDヒントと:https://www.brentozar.com/pastetheplan/ ?ID = B1W ___ N87
select *ヒントなし:https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select *ヒントと:https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

OPTION(LOOP JOIN)ヒントを使用すると、ヒントのselect c.IDないバージョンに比べて読み取りの数が大幅に削減されましたが、それでもselect *、ヒントのないクエリの約4倍の読み取りが行われています。クエリに追加するOPTION(RECOMPILE, HASH JOIN)と、select *他のどの試みよりもパフォーマンスが大幅に低下しました。

使用して表の統計とそのインデックスを更新した後WITH FULLSCANselect c.IDクエリははるかに高速で実行されます。
select c.ID更新前:https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select *更新前:https://www.brentozar.com/ ?pastetheplan / ID = ryrvodEUX
select c.ID:更新後https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select *更新後:https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *select c.ID合計継続時間と合計読み取りの点では依然として優れています(読み取りのselect *約半分)が、より多くのCPUを使用します。全体として、更新前よりもはるかに近いですが、計画はまだ異なります。

同じ動作が、2014互換モードで実行されている2016年と2014年に見られます。2つの計画の不一致を説明できるものは何ですか?「正しい」インデックスが作成されていない可能性がありますか?統計がわずかに古いためにこれが発生する可能性がありますか?

ON部を結合の一部まで複数の方法で移動しようとしましたが、クエリプランは毎回同じです。

インデックスの再構築後

クエリに関係する3つのテーブルのすべてのインデックスを再構築しました。 c.IDまだほとんどの読み取りを実行しています(2倍以上*)が、CPU使用率は*バージョンの約半分です。c.IDまたのソートにtempdbをこぼしたバージョンATable
c.IDhttps://www.brentozar.com/pastetheplan/?id=HyHIeDO87
*https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

また、並列処理なしで強制的に動作させてみたところ、最高のパフォーマンスのクエリが得られました:https : //www.brentozar.com/pastetheplan/?id=SJn9-vuLX

順序付けを行う大きなインデックスシーク後の演算子の実行カウントは、シングルスレッドバージョンでは1,000回しか実行されませんが、並列化バージョンでは2,622〜4,315回のさまざまな演算子の実行で大幅に増加しました。

回答:


4

より多くの列を選択することは、クエリの要求された結果を取得するために、SQL Serverがより一生懸命に作業する必要があることを意味することは事実です。クエリオプティマイザーが両方のクエリに対して完全なクエリプランを作成できた場合、SELECT *すべてのテーブルからすべての列を選択するクエリよりも長く実行するクエリ。クエリのペアの反対を観察しました。コストを比較するときは注意する必要がありますが、低速クエリの合計推定コストは1090.08オプティマイザーユニットであり、高速クエリの合計推定コストは6823.11オプティマイザーユニットです。この場合、オプティマイザーはクエリの合計コストを見積もるのに貧弱な仕事をしていると言えます。SELECT *クエリに対して別のプランを選択し、そのプランがより高価になると予想していましたが、ここではそうではありませんでした。このタイプの不一致は多くの理由で発生する可能性があり、最も一般的な原因の1つはカーディナリティー推定の問題です。オペレーターのコストは、主にカーディナリティーの見積もりによって決まります。計画の重要なポイントでのカーディナリティの推定値が不正確な場合、計画の総費用は現実を反映していない可能性があります。これは非常に単純化されすぎていますが、ここで何が起こっているかを理解するのに役立つことを願っています。

まず、SELECT *クエリが単一の列を選択するよりも高価になる理由について説明します。SELECT *クエリオプティマイザは、それが必要とするすべての列を取得するために追加の作業を行う必要があるか、それがより大きなインデックスから読み取るする必要があるかもしれないことを意味するかもしれない非カバーリング・インデックスの中にいくつかのカバーのインデックスを回すことがあります。SELECT *クエリの実行中に処理する必要がある中間結果セットが大きくなる場合もあります。両方のクエリの推定行サイズを見ると、実際にこれを確認できます。高速クエリでは、行サイズは664バイトから3019バイトの範囲です。低速クエリでは、行サイズの範囲は19〜36バイトです。並べ替えやハッシュビルドなどのブロック演算子は、SQL Serverが大量のデータを並べ替えたり、ハッシュテーブルに変換したりするとコストが高くなることを知っているため、行サイズが大きいデータのコストが高くなります。

高速クエリを見ると、オプティマイザーは240万のインデックスシークを実行する必要があると推定しますDatabase1.Schema1.Object5.Index3。それが、計画コストの大部分の源泉です。しかし、実際の計画では、その演算子で実行されたインデックス検索は1332のみであることが明らかになっています。これらのループ結合の外側部分の実際の行と推定行を比較すると、大きな違いが見られます。オプティマイザーは、クエリの結果に必要な最初の1000行を見つけるために、さらに多くのインデックスシークが必要になると考えています。そのため、クエリのコストは比較的高くなりますが、すぐに終了します。最も高価であると予測された演算子は、予想される作業の0.1%未満しか実行しませんでした。

遅いクエリを見ると、ほとんどがハッシュ結合のプランが得られます(ローカル変数を処理するためだけにループ結合があると思います)。カーディナリティの推定値は完全に完全ではありませんが、実際の推定値の問題はソートの最後にあります。ほとんどの時間は数億行のテーブルのスキャンに費やされていると思います。

クエリの両方のバージョンにクエリヒントを追加して、他のバージョンに関連付けられたクエリプランを強制すると便利な場合があります。クエリヒントは、オプティマイザーがいくつかの選択を行った理由を把握するための優れたツールになります。クエリに追加OPTION (RECOMPILE, HASH JOIN)するSELECT *と、ハッシュ結合クエリに似たクエリプランが表示されます。また、行のサイズがはるかに大きいため、ハッシュ結合プランのクエリコストがはるかに高くなると予想しています。そのため、ハッシュ結合クエリがクエリに選択されなかったのはそのためSELECT *です。OPTION (LOOP JOIN)1つの列のみを選択するクエリに追加すると、次のクエリプランに似たクエリプランが表示されると思います。SELECT *クエリ。この場合、行サイズを小さくしても、クエリ全体のコストに大きな影響はありません。キー検索をスキップすることもできますが、それは推定コストのわずかな割合です。

要約すると、SELECT *クエリを満たすために必要な大きな行サイズは、オプティマイザをハッシュ結合プランではなくループ結合プランに押しやると予想しています。ループ結合計画のコストは、カーディナリティーの見積もりの​​問題によるものよりも高くなります。列を1つだけ選択して行サイズを小さくすると、ハッシュ結合プランのコストは大幅に削減されますが、ループ結合プランのコストにはほとんど影響しないため、効率の低いハッシュ結合プランになります。匿名化された計画についてこれ以上のことを言うのは難しいです。


広範かつ有益な回答をありがとうございました。あなたが提案したヒントを追加してみました。それは作りましたselect c.IDはるかに高速クエリを、まだことをいくつかの余分な仕事をしているselect *のヒントなしでクエリは、ありません。
L.ミラー

2

統計が古くなっていると、オプティマイザがデータを見つけるのに適切でない方法を選択する可能性があります。あなたはインデックスをUPDATE STATISTICS ... WITH FULLSCANフルREBUILDにしようとしたことがありますか?それを試して、それが役立つかどうかを確認してください。

更新

OPの更新によると:

を使用してテーブルとそのインデックスの統計を更新した後WITH FULLSCANselect c.IDクエリははるかに高速に実行されます

だから、今、唯一のアクションが取られた場合だったUPDATE STATISTICSインデックスやってみた後、REBUILD(ではないがREORGANIZE、私は両方の推定行数とその助けを見てきたように)UPDATE STATISTICSとインデックスがREORGANIZEありませんでした。


週末に関係する3つのテーブルのすべてのインデックスを取得して再構築し、それらの結果を反映するように投稿を更新しました。
L.ミラー

-1
  1. インデックススクリプトを含めていただけますか?
  2. 「パラメータスニッフィング」で発生する可能性のある問題を排除しましたか?https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. 私はこのテクニックがいくつかの場合に役立つことを発見しました

    a)次のルールに従って、各テーブルをサブクエリとして書き直します:b
    )SELECT- 結合列を最初に配置c)PREDICATES-それぞれのサブクエリに移動
    d)ORDER BY-それらに移動それぞれのサブクエリは、JOIN COLUMNS FIRSTで並べ替えます
    e)最終的な並べ替えとSELECTのラッパークエリを追加します。

アイデアは、各副選択内で結合列を事前にソートし、各選択リストの最初に結合列を置くことです。

これが私が意味することです...

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1.インデックスDDLスクリプトを元の投稿に追加しようとしますが、「スクラブ」に時間がかかる場合があります。2.実行前にプランキャッシュをクリアすることと、バインドパラメーターを実際の値に置き換えることの両方により、この可能性をテストしました。3.これを試みましたORDER BYが、TOP、FORXMLなどのないサブクエリでは無効ORDER BYです。句なしで試しましたが、同じ計画でした。
L.ミラー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.