「なぜIndex Only Scanが効かないのか」――Visibility Mapの深淵を覗く
PostgreSQLのチューニングにおいて、誰もが一度は「Index Only Scan(IOS)」という聖杯を追い求める時期があるはずです。インデックスだけでクエリが完結すれば、ヒープ(テーブル本体)へのランダムアクセスを排除し、I/Oコストを劇的に下げられる。まさに夢のような話ですよね。
しかし、現実はそう甘くない。`EXPLAIN`を叩いて `Index Scan` と表示された瞬間に、「あれ、ちゃんとカバリングインデックスを作ったはずなのに……」と頭を抱えた経験がある方も多いのではないでしょうか。
今日は、その「なぜ?」の正体であるVisibility Map(VM)と、PostgreSQLがどのようにして「データが本当に有効か」を判断しているのか、その内部アーキテクチャの核心に切り込んでみたいと思います。
—
1. Visibility Map:PostgreSQLの「信頼の証」
Index Only Scanが実現されるためには、単純にインデックスにデータが含まれているだけでは不十分です。PostgreSQLのMVCC(多版同時実行制御)というアーキテクチャ上、「そのデータがどのトランザクションからも参照可能で、かつ最新であるか」を保証しなければならないからです。
ここで登場するのが Visibility Map です。
VMは、データページごとに「このページ内の全てのタプルは、全トランザクションから可視(Visible)である」という情報をビットで保持しています。PostgreSQLはクエリ実行時、インデックスからエントリを見つけると、まずそのエントリが指し示すヒープページがVM上で「All-Visible」かどうかを確認します。
- All-Visibleが立っている場合:
ヒープページを見に行く必要はありません。「このページ内の全タプルは絶対に可視だ」と分かっているため、インデックスの情報だけで結果を確定できる。これがIndex Only Scanの正体です。
- All-Visibleが立っていない場合:
PostgreSQLは「念のため」ヒープページを読みに行きます。MVCCの整合性を保つため、トランザクションID(XMIN/XMAX)を直接確認しに行く必要があるからです。
つまり、カバリングインデックスを作ったのにIOSが発動しない最大の理由は、ヒープ側のVMビットが立っていないからに他なりません。
2. なぜVMビットは落ちるのか?(トラブルシューティングの勘所)
実務でよくあるのが、「データ更新が頻繁なテーブルでのIOSの脱落」です。
VMビットを立てる(あるいは維持する)のは、主に `VACUUM` の仕事です。`VACUUM` はテーブルをスキャンし、全タプルが可視であると判断したページにVMビットをセットします。しかし、更新(UPDATE)や削除(DELETE)が頻発すると、ページ内のタプルが不可視になったり、新しいバージョンが生成されたりするため、VMビットは容赦なくクリアされます。
ここでエンジニアが意識すべきは以下の点です:
- VACUUMの頻度と追従性:
`autovacuum_vacuum_scale_factor` が大きすぎると、VMビットの更新が追いつきません。特に、インデックスだけを頻繁に参照するワークロードでは、VACUUMがVMをクリーンに保てるようにチューニングを詰める必要があります。
- ヒープの肥大化(Bloat):
更新が多いテーブルでVMビットが立たないのは、そもそもヒープが断片化しすぎているシグナルでもあります。`pg_stat_user_tables` で `n_dead_tup` を確認し、VMが機能不全に陥っていないかチェックしてください。
3. 「Index Only Scan」への執着が招く罠
最後に、一つだけ警鐘を鳴らしておきたいことがあります。
確かにIndex Only Scanは高速ですが、それを実現するために「何でもかんでもインデックスに含める(INCLUDE句を使う)」のは慎重になるべきです。インデックスはメモリ(Buffer Cache)を消費し、書き込み時のメンテナンスコストも増大させます。
「クエリを速くしたい」という目的のためにインデックスを肥大化させ、結果として `shared_buffers` の効率が落ちて全体のスループットが低下しては本末転倒です。
「本当にそのクエリは、VMが機能するほど頻繁に実行されるのか?」
「ヒープアクセスを許容した通常のIndex Scanと、コスト差はどれくらいあるのか?」
`EXPLAIN ANALYZE` を叩いて、実行計画のコストだけでなく、`Heap Fetches` の数に注目してください。`Heap Fetches` がゼロであれば、それはVMが完璧に機能している証拠です。もし数字が大きければ、そこに最適化の余地がある。
—
PostgreSQLの深淵は、こうした地味なビットの積み重ねに隠されています。派手なクエリチューニングも良いですが、時には `VACUUM` の挙動やVMのビットの状態に思いを馳せてみる。そんな視点を持つだけで、あなたのデータベースエンジニアとしての解像度は、一段上のレベルに引き上げられるはずです。
さて、次はどのインデックスを最適化しましょうか。
コメント