【テクニカル・上級編】 インデックスオンリースキャン – PostgreSQL

Index-Only Scanの深淵 — PostgreSQLの「真の高速化」を手繰り寄せる

データベースのパフォーマンスチューニングにおいて、私たちは常に「I/Oをいかに減らすか」という問いと対峙しています。その究極の解の一つが、PostgreSQLの「インデックスオンリースキャン(Index-Only Scan)」です。

多くのエンジニアが「インデックスを貼れば速くなる」と信じていますが、実はその先の「インデックスだけでクエリを完結させる」という最適化こそが、高負荷なプロダクション環境で差を生む境界線となります。今日は、この強力な手法の裏側にある、少しだけ泥臭いアーキテクチャの話をしましょう。

—

なぜ「インデックス」だけでは足りないのか

まず基本の確認ですが、PostgreSQLにおいてインデックスは、あくまで「ポインタ」です。インデックスには物理的な行位置(TID: Tuple Identifier)が記録されていますが、その先にある実データ(ヒープ)にアクセスしなければ、データの「可視性(MVCC情報)」を確認できないようになっています。

つまり、通常のインデックススキャンは、以下の2ステップを必ず踏みます。
1. インデックスを辿り、TIDを特定する。
2. ヒープ(データブロック)を読み込み、現在のトランザクションからその行が見えるかチェックする。

この「2ステップ目」のヒープアクセスこそが、ランダムI/Oを誘発し、パフォーマンスを削る主犯です。これを回避するのがIndex-Only Scanの目的ですが、そのためには「ヒープを見ずに、どうやって可視性を判断するのか?」という難問をクリアしなければなりません。

Visibility Mapという名の「カンニングペーパー」

PostgreSQLがこの難問を解決するために用意したのが、Visibility Map(VM)です。

これはテーブルの各データブロックが「そのブロック内の全ての行が、全てのトランザクションから可視であるか?」という情報を保持しているビットマップです。

Index-Only Scanが実行される際、プランナは以下のように振る舞います。
1. インデックスからTIDを取得。
2. そのTIDが指すデータブロックのVMを確認。
3. もし「そのブロックの全行が可視」とマークされていれば、ヒープアクセスを省略してデータを返却。

これがIndex-Only Scanの正体です。非常にエレガントですが、裏を返せば、このVMが最新の状態に更新されていない限り、PostgreSQLは安全のためにヒープを見に行きます。これが、インデックスを貼ったはずなのに「なぜかIndex Scanにしかならない」という現象の正体であることが多いのです。

よくあるパフォーマンストラブルの「犯人」

現場でトラブルシューティングをしていると、Index-Only Scanが効かない原因は決まってこの3つに集約されます。

  • VACUUMが追いついていない(VMがクリアされていない)

更新頻度が高いテーブルでは、VMが更新される前に次の変更が入ることがあります。オートバキュームのチューニングが甘いと、いつまでもIndex-Only Scanが発動しません。

  • クエリが「全ての列」を要求している

`SELECT ` を実行していませんか? インデックスに含まれない列を一つでも要求すれば、エンジンはヒープを見に行くしかありません。必要なカラムだけを定義する徹底が必要です。

  • 統計情報の鮮度不足

プランナが「Index-Only Scanをしても、結局ヒープを見る必要がある行が多い」と判断すれば、コスト計算の結果、通常のインデックススキャンを選択します。`ANALYZE` が適切に走っているかは、意外と見落とされがちです。

匠のチューニング:INCLUDE句の活用

もし、特定のクエリのために「巨大なインデックス」を貼りすぎてインデックスサイズが肥大化し、逆にキャッシュヒット率が下がっているなら、PostgreSQL 11以降で導入された `INCLUDE` 句を検討してください。

CREATE INDEX idx_orders_customer_id ON orders (customer_id) INCLUDE (total_amount);

このように `INCLUDE` を使えば、検索キーには含まれないがIndex-Only Scanで取得したいカラムを、インデックスの「葉」の部分だけに付加できます。インデックスのツリー構造を肥大化させずに、Index-Only Scanの恩恵だけを享受する。これぞ、エンジニアの腕の見せ所です。

—

最後に:インデックスは「コスト」である

最後に一つだけ。どんなにIndex-Only Scanが魅力的でも、インデックスはあくまで「書き込みコスト」を増大させる負債でもあります。

「すべてのクエリをIndex-Only Scanにする」のは、多くの場合、やりすぎです。アクセス頻度の高い特定のクエリ、あるいは巨大なテーブルに対する集計クエリなど、ビジネス上もっとも価値のある場所にこそ、この技術をピンポイントで投入してください。

PostgreSQLは、その内部構造を理解すればするほど、期待に応えてくれるデータベースです。ぜひ皆さんの環境でも、`EXPLAIN (ANALYZE, BUFFERS)` を叩いて、VMが実際にどれだけI/Oを節約しているかを確認してみてください。その「Heap Fetches」の数字が0になった瞬間、エンジニアとしての密かな喜びを感じるはずですよ。

コメント

タイトルとURLをコピーしました