PostgreSQLの「隠し味」、カバリングインデックス(INCLUDE句)でクエリを爆速にする方法
「クエリのパフォーマンスが微妙だな」と思って`EXPLAIN ANALYZE`を叩いたとき、`Heap Fetches`という文字を見て溜め息をついたことはないかな?
PostgreSQLを使っていて、インデックスを貼っているのに期待したほど速くならない原因の多くは、この「ヒープアクセス」にあるんだ。インデックスで場所を特定しても、結局「中身(レコードそのもの)」を見にテーブル本体まで走っていく。これが大きなテーブルになればなるほど、I/O負荷として跳ね返ってくるわけだね。
今日は、その無駄な往復をなくして、インデックスだけで完結させる魔法のような機能、「カバリングインデックス(INCLUDE句)」について話をしよう。
—
そもそも、なぜ「インデックスだけ」ではダメなのか
通常、PostgreSQLのB-treeインデックスは、指定したカラムの値を保持しているだけだ。「このIDのレコードは、データファイルのこの場所にあるよ」というポインタは持っているけれど、そのレコードの他のカラムの値までは持っていない。
例えば、こんなテーブルがあるとする。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT,
status TEXT,
created_at TIMESTAMP
);
ここで「特定のステータスのユーザーのメールアドレスが知りたい」というクエリを投げるとする。
SELECT email FROM users WHERE status = ‘active’;
`status`にインデックスを貼っていても、PostgreSQLは「`active`なID」をインデックスから探し出した後、わざわざヒープ領域(テーブル本体)まで行って、そのIDに対応する`email`を読みに行かなきゃいけない。これが「Index Scan」だ。
もし、インデックスの中に`email`というデータが最初から埋め込まれていたらどうなるか? テーブルを見に行く必要すらなくなる。これが「Index Only Scan」だ。
INCLUDE句:最強の隠し味
PostgreSQL 11から導入された`INCLUDE`句を使えば、これが簡単に実現できる。
CREATE INDEX idx_users_status_email
ON users (status) INCLUDE (email);
こうすると、`status`は検索のキー(B-treeの構造の一部)として使われ、`email`はインデックスのリーフページに「おまけ」として格納される。
これがなぜ凄いのか?
- キーにならないデータを含められる: `email`は検索の条件式(WHERE句)には使わないよね? 普通のインデックスに含めるとソートの順序に影響するけど、`INCLUDE`なら検索効率を落とさずにデータを「同伴」させられるんだ。
- ヒープへのアクセスを完全にカット: クエリに必要なカラムがすべてインデックス内に揃っていれば、PostgreSQLはテーブル本体を一切見ない。I/Oが激減するから、爆速になるのは必然だよ。
実務で使うときの注意点:魔法の使いすぎに注意
「じゃあ、すべてのカラムをINCLUDEに入れればいいじゃん!」と思った君、ちょっと待って。それはデータベースエンジニアとしての罠だ。
1. インデックスの肥大化:
INCLUDEすればするほど、インデックスのサイズは当然大きくなる。インデックスがメモリに乗らなくなれば、結局ディスクI/Oが発生して本末転倒だ。
2. 更新コストの増大:
テーブルのデータが更新されるたびに、インデックスも更新しなきゃいけない。INCLUDEに含めたカラムが頻繁に更新されるカラムだと、書き込み性能がガタ落ちするよ。
3. VM(Visibility Map)との関係:
Index Only Scanが本当に効くかどうかは、そのデータが「最新」だとシステムが判断できるかどうかに依存する。`VACUUM`が適切に動いていないと、せっかくのカバリングインデックスも空振りしてヒープを見に行くことがあるから、運用管理は基本を忘れずにね。
どんなときに使うべきか
僕が現場でよく勧めるのは、「頻繁に叩かれるけど、更新頻度が低い参照専用に近いカラム」に対してだ。
- ユーザーの属性情報(メールアドレス、名前など)
- 定型的な集計で使う中間フラグ
- ログテーブルなどの「書き込みは多いけど、読み取りは特定のキーで特定カラムだけを抜き出す」ようなケース
まとめ
カバリングインデックスは、PostgreSQLのパフォーマンスを一段上のレベルに引き上げる強力なツールだ。
「インデックスを貼ったのに遅い」と感じたときは、`EXPLAIN`の結果を見てみてほしい。`Heap Fetches`が大量に発生しているなら、そのカラムを`INCLUDE`でインデックスの中に引き込んでやるだけで、劇的に改善することがある。
ただし、銀の弾丸はない。インデックスは「読み取りを速くするために書き込みを犠牲にする」トレードオフだということを忘れず、まずは計測から始めてみてくれ。
現場からは以上! また何かあったら聞いてくれよな。
コメント