インデックスオンリースキャンの「隠れた立役者」、Visibility Mapの深淵へ
PostgreSQLのパフォーマンスチューニングにおいて、`Index Only Scan`が奏功した時のあの快感。クエリ実行計画を見て、`Heap Fetches: 0`という文字が躍った瞬間の安堵感。DBエンジニアなら誰もが一度は味わったことがあるはずです。
しかし、なぜPostgreSQLはわざわざヒープ(テーブル本体)を見に行かずに、「インデックスの中の情報だけで真実(可視性)を確信できる」のでしょうか。その裏側で静かに、しかし強力に機能しているのがVisibility Map (VM) です。
今日は、この「縁の下の力持ち」の内部構造を紐解き、なぜこれがパフォーマンスのボトルネックになり得るのか、そしてどう対峙すべきかについて、少し深く掘り下げてみたいと思います。
—
Visibility Mapの正体:ビットが握る「全知」
Visibility Mapは、各テーブルファイル(`_vm`というサフィックスを持つファイル)に対応するビットマップです。役割は非常にシンプルで、「このページ内の全てのタプルは、現在稼働中のどのトランザクションから見ても可視である」ということを、ビットのON/OFFで示します。
VMの各ビットは1つのページ(8KB)に対応しています。あるビットが立っている(`all_visible`)ということは、以下の2点をPostgreSQLが保証していることを意味します。
1. そのページ内の全タプルが、MVCCの可視性ルールをクリアしている。
2. そのページ内のタプルは、将来的に更新されない限り、可視性が変わることはない。
この「保証」があるからこそ、インデックスオンリースキャンにおいて、PostgreSQLはヒープ上のタプルヘッダー(`xmin`, `xmax`など)をわざわざ読みに行く必要がなくなるのです。これを確認するためにヒープをフェッチすることは、ディスクI/Oの観点から見れば非常に高コストですからね。
パフォーマンストラブルシューティング:なぜVMは「嘘」をつくのか?
「VMがあるのに、なぜIndex Only Scanが遅いのか?」という相談を受けることがあります。多くの場合、犯人はVacuumの不全、あるいは頻繁すぎる更新にあります。
1. 「All Visible」が設定されないケース
VMのビットを立てるのは、基本的に`VACUUM`の仕事です。もしデータベースの負荷が高すぎてVacuumが追いついていない、あるいは`autovacuum`の設定が保守的すぎて実行頻度が低い場合、いつまで経ってもビットが立ちません。結果として、インデックスオンリースキャンは「読み取り専用」の皮を被った「ヒープ読み込み」へと劣化します。
2. ビットの「無効化(Invalidation)」コスト
ここが盲点になりがちです。VMは一度ビットが立てば永久不滅ではありません。ページ内のタプルが`UPDATE`や`DELETE`によって変更された瞬間、そのページのビットは即座にクリアされます。
もし、高頻度で更新されるテーブルに対してインデックスオンリースキャンを多用しようとすると、更新のたびにVMが更新され、それがバッファキャッシュの競合を引き起こす……という皮肉な事態に陥ることがあります。
実践的なチューニングの勘所
VMを味方につけるために、私が現場で意識しているポイントをいくつか共有します。
- `visibilitymap_get_status` を活用する:
システムの挙動が怪しいとき、`pg_visibility`拡張機能を使って特定のページの可視性ビットを確認してみてください。想定以上にビットが立っていない場合、それはVacuumの設計を見直すサインです。
- `fillfactor` の戦略的利用:
更新が多いテーブルであれば、あえて`fillfactor`を下げてページに余裕を持たせる手法があります。ページ内の更新頻度とVMの有効率のバランスをどう取るか。ここを制御できると、チューニングの幅が一段階上がります。
- `VACUUM FREEZE`の影響を理解する:
古いタプルが放置されると、Vacuumが「可視」と判断できず、ビットが立たない期間が長くなります。`autovacuum_vacuum_scale_factor`だけでなく、`autovacuum_freeze_max_age`などのパラメータが、巡り巡ってVMの効率に直結していることを忘れないでください。
—
最後に
Visibility Mapは、PostgreSQLの「MVCCという複雑な仕組み」と「ストレージI/Oの物理的な制約」の間を取り持つ、非常に洗練されたインターフェースです。
単に「インデックスを貼れば速くなる」という段階から一歩進んで、「なぜこのクエリはヒープを見に行かずに済んでいるのか?」というレベルまで想像力を広げると、PostgreSQLというデータベースの解像度がグッと高まります。
皆さんのクエリが今日も`Heap Fetches: 0`で駆け抜けることを願っています。さて、次はどの内部構造の皮を剥いでいきましょうか?
コメント