「テーブルを見に行かない」という贅沢。PostgreSQLのインデックスオンリースキャンを使い倒す話
現場でパフォーマンスチューニングをしていると、必ずぶち当たる壁があるよね。「インデックスを貼っているはずなのに、なぜかクエリが遅い」というやつだ。
explain analyzeの結果を見て、「よし、Index Scanが効いてるからバッチリだ!」なんて思っていたら、実はまだ改善の余地があるかもしれない。今日は、PostgreSQLのパフォーマンスを極限まで引き出すための切り札、「インデックスオンリースキャン(Index Only Scan)」について深掘りしていくよ。
—
なぜ「ヒープ」へのアクセスがボトルネックになるのか?
PostgreSQLでインデックスを使うとき、通常は「インデックスを読んでから、実体であるヒープ(テーブル)のデータを取りに行く」という手順を踏むよね。
インデックスには「どの行がどこにあるか(TID)」という住所しか書かれていないから、実際の値を知るためには、わざわざ本棚(ヒープ)まで本を取りに行かなきゃいけない。この「インデックスの検索」+「ヒープの参照」という2ステップが、データ量が膨大になると大きな負荷になるんだ。
そこで登場するのが、インデックスオンリースキャン。
「わざわざ本棚まで行かなくても、目次(インデックス)だけで答えが分かっちゃいますよね?」という、賢い最適化手法だ。
Visibility Map(可視性マップ)という秘密兵器
でも、ちょっと待って。「インデックスに欲しいデータが載っている」だけじゃ足りないんだ。PostgreSQLはMVCC(多版同時実行制御)を採用しているから、「そのデータは本当に最新か?」「他のトランザクションから見て可視か?」を確認する必要がある。
ここで役立つのが、Visibility Map(可視性マップ)という仕組みだ。
PostgreSQLは、あるページ内の全ての行が「全てのトランザクションから可視である」ことを知っている場合、そのページに対応するVisibility Mapのビットを立てる。データベースはこれを見ることで、「あ、このページはもう誰からも更新されない(コミット済みだ)から、ヒープまでわざわざ確認に行かなくてもいいや」と判断できる。
つまり、インデックスオンリースキャンが成立する条件はこれだ。
1. クエリに必要な列が、すべてインデックスに含まれていること。
2. 対象データの多くが「可視(Visible)」であること。
実践:どうやってインデックスオンリースキャンを誘発するか?
例えば、ユーザーのメールアドレスを頻繁に検索するシステムがあるとする。
— よくあるクエリ
SELECT email FROM users WHERE status = ‘active’;
普通に `status` カラムにインデックスを貼っても、`email` を取得するためにヒープへアクセスしちゃうよね。ここで、カバリングインデックスというテクニックを使う。
— 複合インデックスでクエリを「包み込む(Covering)」
CREATE INDEX idx_users_status_email ON users (status, email);
こうすると、`status` で絞り込みつつ、欲しい `email` もインデックスの中に存在することになる。これでPostgreSQLは、ヒープへ寄り道することなく、インデックスだけでクエリを完結させてくれるようになるんだ。
注意:VACUUMが効かないと「遅い」罠
ここで現場のエンジニアがハマりやすい落とし穴がある。「インデックスオンリースキャンを狙ったのに、なぜかIndex Scan(ヒープ参照あり)になってしまう」というケースだ。
最大の原因は、VACUUMが追い付いていないこと。
テーブルが更新されると、PostgreSQLはその行が「まだ可視かどうかわからない」と判断して、わざわざヒープを見に行こうとするんだ。もし君のDBでオートバキュームの設定が甘かったり、更新頻度に対してVACUUMが追いついていなかったりすると、Visibility Mapのビットが立たず、インデックスオンリースキャンが発動しなくなる。
「なぜか最近パフォーマンスが落ちたな?」と思ったら、`pg_stat_user_tables` を見て `n_dead_tup` が増えすぎていないかチェックしてみてほしい。
まとめ:チューニングの心得
インデックスオンリースキャンは強力だけど、何でもかんでも列を追加して複合インデックスを作ればいいわけじゃない。インデックスが増えれば、今度は `INSERT` や `UPDATE` が重くなるというトレードオフがあるからね。
- 頻繁に実行されるSELECTクエリを特定する
- そのクエリで必要な列をカバリングインデックスに含める
- VACUUMが適切に動く環境を整える
この3つを意識するだけで、アプリのレスポンスは劇的に変わるはずだ。教科書的な知識も大事だけど、結局は「データベースが裏でどう動いているか」を想像する力が、エンジニアとしての武器になる。
次はぜひ、自分のプロジェクトの `EXPLAIN (ANALYZE, BUFFERS)` を叩いてみてほしい。ヒープへの参照(Heap Fetches)が「0」になった瞬間の快感、ぜひ味わってくれよな!
コメント