Index Only Scanという「聖杯」を追い求めて:PostgreSQLの内部構造から紐解くパフォーマンスチューニング
PostgreSQLを触り始めてしばらく経つと、誰もが一度は「なぜこのクエリは、インデックスを使っているはずなのに、これほどまでにディスクI/Oを叩くのか?」という壁にぶつかります。
インデックスを貼ったのにクエリが遅い。実行計画(`EXPLAIN`)を見ると、確かにインデックスは使われているけれど、相変わらず「Heap Fetches」が発生している……。
今日は、そんな皆さんのために、PostgreSQLにおける最適化の「聖杯」とも言えるIndex Only Scan(インデックスオンリースキャン)について、少し深く掘り下げてみたいと思います。教科書的な定義をなぞるのではなく、現場で戦うエンジニアとして、この仕組みが何を代償にし、どうすれば最大限に引き出せるのかを語りましょう。
—
そもそも、なぜ「インデックス」だけでは完結しないのか
PostgreSQLのアーキテクチャにおいて、インデックスはあくまで「データへのポインタ(TID: Tuple ID)」を保持する地図に過ぎません。インデックスには、データの検索に必要なキー(列の値)は入っていますが、その行が「今、可視(Visible)かどうか」を判断するためのMVCC情報や、他の列のデータは入っていないことがほとんどです。
通常のインデックススキャンは、以下の手順を踏みます。
1. インデックスを検索し、TIDを取得する。
2. そのTIDを元に、ヒープ(テーブル本体)にアクセスして、実際の行データを取得する。
3. 可視性(Visibility)をチェックし、問題なければ結果として返す。
この「ヒープへのアクセス」こそが、パフォーマンスの最大のボトルネックです。ランダムI/Oは、現代のNVMe SSDであっても、シーケンシャルアクセスに比べれば依然としてコストが高い。ここを回避するのが、Index Only Scanの真髄です。
—
Visibility Map(VM)という「抜け道」
では、PostgreSQLはどうやってヒープに触れずに結果を返しているのか。ここで登場するのがVisibility Mapです。
VMは、各ページ内のすべてのタプルが「どのトランザクションからも可視である(つまり、削除や更新がなされていない)」ことを保証するためのビットマップです。
- インデックスにある列だけでクエリが完結する。
- かつ、対象ページがVM上で「All-Visible(すべて可視)」とマークされている。
この条件が揃ったとき、PostgreSQLは「わざわざヒープを見に行かなくても、この行は生きていることが確定している」と判断します。これがIndex Only Scanの仕組みです。
—
VACUUMが「性能の敵」になる瞬間
ここで、多くのエンジニアが陥る罠があります。そう、VACUUMの重要性です。
Index Only Scanの成否は、VMの更新状態に依存します。VMは、主に`VACUUM`プロセスによって更新されます。もし、トランザクションが活発で更新頻度が高いテーブルにおいて、`autovacuum`のチューニングが甘ければどうなるか。
1. 新しいデータが挿入・更新される。
2. VMの「All-Visible」フラグがクリアされる。
3. クエリはVMを信頼できなくなり、ヒープへのアクセスを余儀なくされる。
4. 結果としてIndex Only Scanから通常のIndex Scanへ「格下げ」され、I/Oが急増する。
つまり、Index Only Scanを維持するということは、「テーブルの鮮度を保つためのコストを、バックグラウンドで払い続ける」というトレードオフを受け入れることと同義なのです。
—
トラブルシューティングの現場から
もし皆さんのシステムで「Index Only Scanが効いたり効かなかったりして実行計画が安定しない」という問題に直面したら、まず以下の点を確認してみてください。
- `EXPLAIN (ANALYZE, BUFFERS)` の確認:
`Heap Fetches`が0になっているかを確認してください。もしここの値が予想以上に大きい場合、VMが機能していません。
- `autovacuum`の追い込み:
更新頻度の高いテーブルでは、`autovacuum_vacuum_scale_factor`を下げ、より頻繁にVACUUMが走るように設定します。ただし、CPU負荷との相談です。
- `HOT` (Heap Only Tuple) 更新の活用:
インデックス列以外の更新であれば、インデックスの更新を回避できるHOT更新が機能しているか。`pg_stat_user_tables`の`n_tup_hot_upd`をチェックしましょう。
—
最後に:完璧を求めすぎない勇気
Index Only Scanは非常に強力ですが、全てのクエリに適用しようと躍起になる必要はありません。
インデックスを肥大化させれば(`INCLUDE`句を使ったカバリングインデックスなど)、インデックス自体のメンテナンスコストやメモリ消費が増大します。インデックスは「検索を速くする手段」であり、「それ自体が目的」ではありません。
データベースチューニングの面白いところは、常に何かのトレードオフを突きつけられる点にあります。VMの仕組みを理解し、VACUUMが裏で何をしているかを想像しながらクエリを書く。その一歩先にある「最適解」を探求するプロセスこそ、エンジニアとしての醍醐味ではないでしょうか。
皆さんのデータベースが、今日も健やかに、そして高速に動作することを願っています。それでは、また。
コメント