インデックスは「嘘」をつく:VACUUMとVisibility Mapが握るクエリの深淵
PostgreSQLを長年触っていると、ある日突然、クエリの実行計画(EXPLAIN)が不可解な挙動を見せることがあります。インデックスは完璧に貼ってあるはずなのに、なぜか期待したIndex Only Scanが選ばれない。あるいは、VACUUMが走った直後にパフォーマンスが劇的に変わる(あるいは悪化する)。
これらはすべて、PostgreSQLの「MVCC(多版同時実行制御)」という巨大な仕組みの裏側で、Visibility Map(可視性マップ)とインデックスがどうダンスを踊っているかを知ることで氷解します。今日は、少し深掘りしてみましょう。
インデックスは「誰が見えるか」を知らない
まず、大前提として理解しておくべきことがあります。PostgreSQLのインデックス(B-treeなど)は、テーブルの物理的な「行のコピー」を保持していますが、その行が「現在誰から見えているか」というコンテキストを完全には持ち合わせていません。
インデックス内のエントリには、その行が属するヒープページへのポインタ(TID)があるだけです。クエリがインデックスを参照する際、PostgreSQLは「このTIDの行は、今のトランザクションから見て有効(Visible)か?」を確認するために、必ずヒープページを覗きに行く必要があります。
これが、通常のインデックススキャンがヒープへのランダムアクセスを伴い、コストが高くなる理由です。
Visibility Map:クエリプランナの「虎の巻」
ここで登場するのが Visibility Map(VM) です。これは、各データページが「その中のすべての行が、すべての実行中のトランザクションから見て可視であるか」をフラグで管理するビットマップです。
VMが提供する「全行可視」という情報は、プランナにとって宝の山です。
- Index Only Scanの実現:
もしVMが「このページは全行可視だ」と教えてくれれば、わざわざヒープページまで行を確認しに行く必要はありません。インデックスの情報だけで結果を確定できる。これがPostgreSQLにおける「Index Only Scan」の正体です。
- プランナの最適化:
プランナは「テーブルの全ページがVMで『全行可視』とマークされているか」を計算し、コスト見積もりに反映させます。VMの情報が不正確だと、プランナは「Index Only Scanはリスクが高い」と判断し、無駄に重いIndex ScanやSequential Scanを選択してしまいます。
VACUUMが「掃除」以上の意味を持つ理由
さて、ここでVACUUMの登場です。VACUUMの役割は、不要になったタプル(Dead Tuple)を回収して領域を再利用することだけだと思われがちですが、実はその副次的な作用こそが重要です。
1. VMの更新: VACUUMは、ヒープページをスキャンしてDead Tupleを掃除しながら、「もうこのページには古いトランザクションから見えるような古いデータはない」と判断すると、VMのビットを立てます。
2. インデックスのクリーンアップ: もしVACUUMが適切に行われないと、インデックス内にも不要なTIDエントリが残り続け、インデックスサイズが肥大化します。これはキャッシュ効率を下げ、I/Oを増幅させます。
つまり、「VACUUMが遅れる=VMが更新されない=プランナが最適解を見失う=インデックスが肥大化する」という負の連鎖が完成します。
トラブルシューティングの勘所
もし本番環境で「インデックスが効いていない気がする」と感じたら、以下の視点を持ってください。
- `pg_stat_user_tables` の確認:
`n_dead_tup` が異常に高くなっていませんか? Autovacuumの閾値調整が追いついていない証拠です。
- Visibility Mapの鮮度:
`pg_visibility_map` などの拡張モジュールを使えば、VMの可視性状態を可視化できます。特定のページだけがいつまでも「全行可視」にならない場合、そこに更新のホットスポット(頻繁に更新される行)がある可能性が高いです。
- インデックスの物理的肥大化:
`pgstattuple` を使ってインデックスの「デッドな領域」を調べてみてください。論理的には不要なインデックスエントリが、物理的にメモリ(Buffer Cache)を圧迫しているケースは驚くほど多いものです。
最後に:データベースは「生き物」である
データベースのパフォーマンスチューニングは、単なるパラメータ調整ではありません。PostgreSQLが裏で何を見ようとし、何を諦めようとしているのか。その「意志」をインデックスとVMを通じて読み解くことこそが、エンジニアの醍醐味です。
「なぜこのクエリはこう動くのか」——その問いに対する答えの多くは、このMVCCの仕組みの中に隠されています。ぜひ、皆さんの環境でも `EXPLAIN (ANALYZE, BUFFERS)` を叩いて、ヒープへのアクセス回数と、VMがどう作用しているかを眺めてみてください。
機械的な設定のコピー&ペーストを卒業し、PostgreSQLと対話する準備はできましたか?
コメント