データベースの「究極の省エネ」:Index-Only Scanを極める
データベースのパフォーマンスを追求する旅の終着点は、往々にして「いかにI/Oを減らすか」という問いに帰結します。PostgreSQLにおいて、クエリ実行時のI/Oを劇的に削減する最強の武器の一つが、ご存知 Index-Only Scan です。
ただ、多くのエンジニアが「インデックスを貼ればIndex-Only Scanになるはずだ」と期待し、`EXPLAIN ANALYZE` を見て肩を落とす姿を何度も見てきました。なぜ彼らのクエリはテーブル本体(Heap)まで足を運んでしまうのか。今日は、その裏側にある「Visibility Map」という名の立役者と、Index-Only Scanを成立させるための厳格なルールについて、少し深掘りしてみたいと思います。
—
なぜインデックスだけでは不十分なのか
PostgreSQLのMVCC(多版同時実行制御)アーキテクチャでは、データの一貫性を保つために、各行が「どのトランザクションから見えるか」という情報をテーブル本体(Heap)の行ヘッダに持っています。
インデックスはあくまで「値」と「行ポインタ(TID)」の地図に過ぎません。インデックスには「その行が現在有効な状態か」というMVCCの情報が含まれていないのです。そのため、デフォルトの挙動では、インデックスでTIDを特定した後、必ずHeapページへアクセスして「この行は本当に読める状態か?」を確認する必要があります。これが通常のIndex Scanです。
しかし、PostgreSQLは賢い。この「Heapへの確認」をスキップする仕組みを用意しました。それが Index-Only Scan です。
—
Visibility Mapという「証明書」
Index-Only Scanを実現するための鍵が、Visibility Map (VM) です。
VMは、各データファイル(テーブル)ごとに用意されているビットマップファイルです。このビットマップの各ビットは、Heap内の特定のページに対応しており、「そのページ内のすべての行が、すべてのトランザクションから見える(=古いバージョンが存在しない)」という状態であることを示しています。
もしVM上でそのページが「all-visible」であるとマークされていれば、PostgreSQLは「このページに関しては、Heapを見に行かなくてもMVCCの整合性は保証されている」と判断します。これにより、インデックスだけで完結する高速なスキャンが可能になるのです。
—
Index-Only Scanを成功させるための「3つの絶対条件」
現場でIndex-Only Scanを意図的に引き出すには、以下の条件を意識する必要があります。
1. クエリがインデックスのみで完結すること
`SELECT` リストと `WHERE` 句に含まれるすべてのカラムが、インデックスの中に存在しなければなりません。`INCLUDE` 句を使ったカバリングインデックスが威力を発揮するのはここです。
2. 対象データが「all-visible」であること
ここが最大の罠です。どれだけ完璧なインデックスを貼っても、テーブルのデータが頻繁に更新(UPDATE/DELETE)されていると、VMのビットが立たず、Index-Only Scanは発生しません。
3. VMの更新を待つ(あるいは促す)こと
`VACUUM` が実行されるまで、VMは更新されません。データを入れた直後や、大量の更新直後はIndex-Only Scanが効かないことがよくあります。
—
パフォーマンストラブルシューティングの勘所
もし、期待しているIndex-Only Scanが実行されないなら、まずは `EXPLAIN (ANALYZE, BUFFERS)` を見てください。
- Heap Fetches が 0 でなければ、それはIndex-Only Scanではありません。
- `Heap Fetches` が多い場合、それはVMが最新の更新を反映できていないか、そもそもHOT(Heap Only Tuple)更新が効いていない可能性が高いです。
現場での解決策:
- VACUUMの頻度を見直す: 自動バキュームが追いついていないなら、`autovacuum_vacuum_scale_factor` を調整してVMの更新サイクルを早めます。
- HOT更新を活用する: インデックス付けされていないカラムばかりを更新するようにテーブル設計を工夫すれば、行の物理的な場所が変わらず、VMを汚さない更新が可能です。
- リードレプリカでの挙動: レプリカ側でIndex-Only Scanが効かない場合、`hot_standby_feedback = on` を検討してください。主系でのVACUUMによる削除がレプリカ側のクエリを邪魔しないようにする設定ですが、これがVMの更新を安定させる助けになります。
—
最後に:トレードオフを愛する
Index-Only Scanは魔法の杖ではありません。インデックスを広げれば書き込み負荷(Write Amplification)が増え、更新頻度が高ければVMは役に立ちません。
「読み取り専用に近いマスタデータ」なのか、「秒間数千件の更新が走るトランザクションテーブル」なのか。アーキテクチャを理解した上で、そのデータの性格に合わせてインデックスを設計する。これこそが、PostgreSQLを極めるエンジニアの醍醐味ではないでしょうか。
皆さんのクエリが、今日もHeapの海を越えて、最短経路で結果を返してくれることを願っています。
コメント