SQL Serverには、クエリに含まれるさまざまなテーブルに関する知識を使用して、コンパイル中に利用可能な時間内で最適なプランであると判断したものを生成するコストベースのオプティマイザーがあります。この知識には、存在するインデックスとそのサイズ、および存在する列統計が含まれます。最適なクエリプランを見つけるために必要なことの一部は、プランの実行中に必要な物理的な読み取りの数を最小限に抑えることです。
何度か質問されたのは、クエリプランをコンパイルするときにオプティマイザがSQL Serverバッファプールの内容を考慮しない理由です。これにより、クエリの実行が速くなる可能性があります。この投稿では、その理由を説明します。
バッファプールの内容を把握する
オプティマイザーがバッファープールを無視する最初の理由は、バッファープールの編成方法が原因で、バッファープールに何が含まれているかを把握することが重要な問題であるためです。データファイルページは、バッファと呼ばれる小さなデータ構造によってバッファプール内で制御されます。これは、(非網羅的なリスト)などを追跡します。
- ページのID(ファイル番号:ファイル内のページ番号)
- ページが最後に参照されたとき(怠惰なライターが、必要に応じて空き領域を作成する、最も使用頻度の低いアルゴリズムの実装を支援するために使用)
- バッファプール内の8KBページのメモリ位置
- ページが汚れているかどうか(汚れたページには、耐久性のあるストレージにまだ書き戻されていない変更が加えられています)
- ページが属するアロケーションユニット(ここで説明)とアロケーションユニットIDを使用して、ページがどのテーブルとインデックスに属しているかを把握できます
バッファプールにページがあるデータベースごとに、ページID順にページのハッシュリストがあり、ページがすでにメモリにあるかどうか、または物理的な読み取りを実行する必要があるかどうかをすばやく検索できます。ただし、SQL Serverが、テーブルの各インデックスのリーフレベルの何パーセントがすでにメモリにあるかを簡単に判断できるものはありません。コードは、データベースのバッファのリスト全体をスキャンして、問題のアロケーションユニットのページをマップするバッファを探す必要があります。また、データベースのメモリ内のページが多いほど、スキャンにかかる時間が長くなります。クエリのコンパイルの一部として実行するには、法外な費用がかかります。
興味があれば、DMV sys.dm_os_buffer_descriptors を使用して、バッファプールをスキャンし、いくつかのメトリックを提供するT-SQLコードを使用してしばらく前に投稿を書きました。 。
バッファプールの内容を使用すると危険な理由
オプティマイザがクエリプランで使用するインデックスを選択するのに役立つバッファプールの内容を決定するための非常に効率的なメカニズムがあるとしましょう。私が探求しようとしている仮説は、オプティマイザーが、使用する最も効率的な(小さい)インデックスと比較して、効率の低い(大きい)インデックスがすでにメモリ内にあることを十分に知っている場合、メモリ内のインデックスを選択する必要があるというものです。必要な物理読み取りの数を減らすと、クエリの実行が速くなります。
使用するシナリオは次のとおりです。テーブルBigTableには、特定のクエリを完全にカバーする2つの非クラスター化インデックスIndex_AとIndex_Bがあります。クエリでは、クエリ結果を取得するために、インデックスのリーフレベルを完全にスキャンする必要があります。テーブルには100万行あります。 Index_Aのリーフレベルは200,000ページ、Index_Bのリーフレベルは100万ページであるため、Index_Bを完全にスキャンするには5倍以上のページを処理する必要があります。
この不自然な例は、8つのプロセッサコア、32 GBのメモリ、およびソリッドステートディスクを備えたSQLServer2019を実行しているラップトップで作成しました。コードは次のとおりです。
CREATE TABLE BigTable ( c1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b' ); GO INSERT INTO BigTable DEFAULT VALUES; GO 1000000 CREATE NONCLUSTERED INDEX Index_A ON BigTable (c2) INCLUDE (c3); -- 5 records per page = 200,000 pages GO CREATE NONCLUSTERED INDEX Index_B ON BigTable (c2) INCLUDE (c4); -- 1 record per page = 1 million pages GO CHECKPOINT; GO
そして、私は不自然なクエリの時間を計りました:
DBCC DROPCLEANBUFFERS; GO -- Index_A not in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A)); GO -- CPU time = 796 ms, elapsed time = 764 ms -- Index_A in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A)); GO -- CPU time = 312 ms, elapsed time = 52 ms DBCC DROPCLEANBUFFERS; GO -- Index_B not in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B)); GO -- CPU time = 2952 ms, elapsed time = 2761 ms -- Index_B in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B)); GO -- CPU time = 1219 ms, elapsed time = 149 ms
どちらのインデックスもメモリにない場合は、Index_Aが最も効率的に使用できるインデックスであり、クエリの経過時間はIndex_Bを使用した2,761msに対して764msであり、両方のインデックスがメモリにある場合も同様です。ただし、Index_Bがメモリ内にあり、Index_Aがメモリ内にない場合、クエリでIndex_B(149ms)を使用すると、Index_A(764ms)を使用する場合よりも高速に実行されます。
それでは、オプティマイザーがバッファープールの内容に基づいてプランを選択できるようにしましょう…
Index_Aがほとんどメモリになく、Index_Bがほとんどメモリにある場合、その瞬間に実行されるクエリに対して、Index_Bを使用するようにクエリプランをコンパイルする方が効率的です。 Index_Bの方が大きく、スキャンスルーに多くのCPUサイクルが必要ですが、物理読み取りは追加のCPUサイクルよりもはるかに遅いため、より効率的なクエリプランにより、物理読み取りの数が最小限に抑えられます。
この引数は当てはまるだけであり、Index_Bがほとんどメモリに残り、Index_Aがほとんどメモリにない場合、「useIndex_B」クエリプランは「useIndex_A」クエリプランよりも効率的です。 Index_Aのほとんどがメモリ内にあるとすぐに、「Use Index_A」クエリプランがより効率的になり、「useIndex_B」クエリプランは間違った選択になります。
コンパイルされた「UseIndex_B」プランがコストベースの「useIndex_A」プランよりも効率が悪い状況は次のとおりです(一般化):
- Index_AとIndex_Bはどちらもメモリ内にあります。コンパイルされたプランには、ほぼ3倍の時間がかかります。
- どちらのインデックスもメモリに常駐していません。コンパイルされたプランには3.5倍以上の時間がかかります
- Index_Aはメモリに常駐し、Index_Bはそうではありません。プランによって実行されるすべての物理的な読み取りは無関係であり、53倍の時間がかかります。
概要
私たちの思考演習では、オプティマイザーはバッファープールの知識を使用して、最も効率的なクエリを一度にコンパイルできますが、バッファープールの内容が変動する可能性があるため、計画のコンパイルを促進するのは危険な方法であり、キャッシュされた計画は非常に信頼性が低いです。
オプティマイザーの仕事は、適切な計画をすばやく見つけることであり、必ずしもすべての状況の100%に最適な単一の計画であるとは限らないことを忘れないでください。私の意見では、SQL Serverオプティマイザーは、SQL Serverバッファープールの実際の内容を無視することで正しいことを行い、代わりにさまざまなコストルールに依存して、ほとんどの場合最も効率的である可能性が高いクエリプランを生成します。 。