_select *
_を使用すると、読み取りがはるかに少なくなるだけでなく、_select c.Foo
_を使用するよりも大幅に少ないCPU時間を使用するクエリがあります。
これはクエリです:
_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
Joe 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
_select c.ID
_でOPTION(LOOP JOIN)
ヒントを使用すると、ヒントなしのバージョンと比較して読み取り数が大幅に減少しましたが、_select *
_クエリなしの読み取り数の約4倍を実行していますヒント。 _select *
_クエリにOPTION(RECOMPILE, HASH JOIN)
を追加すると、私が試した他のどのクエリよりもパフォーマンスが大幅に低下しました。
_WITH FULLSCAN
_を使用してテーブルとそのインデックスの統計を更新した後、_select 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 *
_の読み取りは約半分です)。全体的に、更新前よりもはるかに近くなっていますが、計画はまだ異なります。
同じ動作は、2014互換モードで実行されている2016と2014でも見られます。2つの計画の違いを説明できるものは何ですか? 「正しい」インデックスが作成されていない可能性がありますか?統計がわずかに古くなっているために、これが発生する可能性がありますか?
述語を結合のON
の部分まで複数の方法で移動しようとしましたが、クエリプランは毎回同じです。
クエリに関連する3つのテーブルのすべてのインデックスを再構築しました。 _c.ID
_は引き続きほとんどの読み取りを行っていますが(_*
_の2倍以上)、CPU使用率は_*
_バージョンの約半分です。 _c.ID
_バージョンも、ATable
のソート時にtempdbに流出しました。
_c.ID
_: https://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の実行で大幅に増加しました。
より多くの列を選択することは、SQL Serverが要求されたクエリの結果を取得するためにより多くの作業を必要とする可能性があることを意味します。クエリオプティマイザーが両方のクエリに最適なクエリプランを作成できた場合、_SELECT *
_クエリは、すべてのテーブルからすべての列を選択するクエリよりも長く実行することを期待できます。クエリのペアの反対を観察しました。コストを比較する場合は注意が必要ですが、遅いクエリの推定コストの合計は1090.08オプティマイザユニットであり、高速のクエリの推定コストは合計で6823.11オプティマイザユニットです。この場合、オプティマイザはクエリの総コストを見積もるのに不十分です。 SELECT *クエリには別のプランを選択し、そのプランはより高価になると予想していましたが、ここではそうではありませんでした。このタイプの不一致は多くの理由で発生する可能性があり、最も一般的な原因の1つは、カーディナリティー推定の問題です。オペレーターのコストは、主に基数の見積もりによって決まります。プランの重要なポイントでの基数の見積もりが不正確である場合、プランの総コストは現実を反映していない可能性があります。これは非常に単純化しすぎですが、ここで何が起こっているのかを理解するのに役立つことを願っています。
まず、_SELECT *
_クエリが単一の列を選択するよりもコストがかかる理由について説明します。 _SELECT *
_クエリは、一部のカバリングインデックスを非カバリングインデックスに変える可能性があります。これは、オプティマイザが必要なすべての列を取得するために追加作業を行う必要があるか、またはより大きなインデックスから読み取る必要がある場合があることを意味します。 _SELECT *
_を使用すると、クエリの実行中に処理する必要のある中間結果セットが大きくなる場合もあります。両方のクエリで推定行サイズを確認することで、この動作を確認できます。高速クエリでは、行サイズの範囲は664バイトから3019バイトです。スロークエリでは、行サイズの範囲は19〜36バイトです。 SQL Serverは、大量のデータを並べ替えたり、ハッシュテーブルに変換したりする方がコストが高いことを認識しているため、並べ替えやハッシュビルドなどのブロック演算子は、行サイズが大きいデータのコストが高くなります。
高速クエリを見ると、オプティマイザは_Database1.Schema1.Object5.Index3
_で240万回のインデックスシークを行う必要があると推定しています。これが、計画コストのほとんどが発生する場所です。しかし、実際の計画では、1333インデックスシークのみがその演算子で実行されたことが明らかになっています。これらのループ結合の外側の部分の実際の行と推定された行を比較すると、大きな違いが見られます。オプティマイザは、クエリの結果に必要な最初の1000行を見つけるには、さらに多くのインデックスシークが必要になると考えています。クエリが比較的高いコストプランを持っているのはそのためです。最も高価であると予測された演算子は、予想される作業の0.1%未満でした。
遅いクエリを見ると、ほとんどハッシュ結合の計画が得られます(ループ結合はローカル変数を処理するためだけにあると思います)。カーディナリティの見積もりは完全ではありませんが、実際の見積もりの問題はソートの最後にあります。ほとんどの時間は数億行のテーブルのスキャンに費やされていると思います。
クエリヒントを両方のバージョンのクエリに追加して、他のバージョンに関連付けられたクエリプランを強制することが役立つ場合があります。クエリヒントは、オプティマイザがいくつかの選択を行った理由を理解するのに適したツールです。 _SELECT *
_クエリにOPTION (RECOMPILE, HASH JOIN)
を追加すると、ハッシュ結合クエリと同様のクエリプランが表示されると思います。また、行のサイズが非常に大きいため、ハッシュ結合プランではクエリのコストがはるかに高くなると予想しています。そのため、_SELECT *
_クエリに対してハッシュ結合クエリが選択されなかったのはそのためです。列を1つだけ選択するクエリにOPTION (LOOP JOIN)
を追加すると、_SELECT *
_クエリと同様のクエリプランが表示されるはずです。この場合、行のサイズを小さくしても、クエリ全体のコストに大きな影響はありません。キールックアップをスキップすることもできますが、これは推定コストのごく一部です。
要約すると、_SELECT *
_クエリを満たすために必要な行のサイズが大きくなると、ハッシュ結合プランではなくループ結合プランにオプティマイザをプッシュします。ループ結合計画のコストは、カーディナリティの見積もりの問題が原因で、本来のコストよりも高くなります。列を1つだけ選択して行サイズを小さくすると、ハッシュ結合計画のコストは大幅に削減されますが、ループ結合計画のコストにはあまり影響しないため、ハッシュ結合計画の効率が低下します。匿名化された計画については、これ以上のことを言うのは難しいです。
古くなった統計により、オプティマイザはデータを見つける方法として不適切な方法を選択する可能性があります。インデックスでUPDATE STATISTICS ... WITH FULLSCAN
を実行したり、完全なREBUILD
を実行したりしましたか?それを試して、それが役立つかどうかを確認してください。
[〜#〜]更新[〜#〜]
O.P.からの更新によると:
WITH FULLSCAN
を使用してテーブルとそのインデックスの統計を更新した後、select c.ID
クエリがはるかに速く実行されます
したがって、ここで、実行された唯一のアクションがUPDATE STATISTICS
の場合は、インデックスREBUILD
を試してください(REORGANIZE
ではなく)。これは、両方のUPDATE STATISTICS
とインデックスREORGANIZE
は含まれていません。