やあ。最近、データベースのパフォーマンスチューニングで頭を悩ませている若手エンジニアから、「インデックスを貼ってもクエリが速くならない!」という相談を受けることが増えてきたんだ。
インデックス設計って、ただ「WHERE句のカラムに貼ればいい」っていう単純な世界じゃない。今日は、PostgreSQLの強力な武器である「カバリングインデックス(INCLUDE句)」について、現場で本当に役立つ話をしようと思う。
—
インデックスは「辞書の索引」じゃない、実は「辞書そのもの」になれる
普段、インデックスをどう捉えているかな? よくある例えだと「本の巻末にある索引」だよね。でも、PostgreSQLのインデックスは、もっと賢いことができる。
もし、インデックスの中に「探したいデータそのもの」がすべて含まれていたらどうだろう? わざわざ元のテーブル(ヒープ領域)まで読みに行く必要がなくなるよね。これがIndex-Only Scanだ。
ここで登場するのが、PostgreSQL 11から導入された `INCLUDE` 句だ。
普通のインデックスとの決定的な違い
例えば、ユーザーのメールアドレスから名前を検索するクエリを考えてみよう。
SELECT name FROM users WHERE email = ‘taro@example.com’;
普通に `email` 列にインデックスを貼ると、PostgreSQLはインデックスを使って `email` を検索し、見つけた行のポインタを頼りに、わざわざテーブルの本体まで「名前(name)」を取りに行く(これをヒープアクセスと言う)。これがI/Oのボトルネックになるんだ。
ここで、こう書き換えてみる。
CREATE INDEX idx_users_email_include_name
ON users (email) INCLUDE (name);
こうすると、`email` は検索用のキーとして使われ、`name` は「付随情報」としてインデックスのデータページ内に物理的に格納される。結果として、PostgreSQLはインデックスだけを見て、`name` を直接返せるようになる。これが劇的に速い理由だ。
—
INCLUDE句を使うべき「現場の判断基準」
「じゃあ、全部のカラムをINCLUDEすればいいのでは?」と思った君。ストップだ。それはデータベースを太らせるだけの愚策だよ。インデックスのサイズが肥大化すれば、メモリ(shared_buffers)を圧迫して、結果的にシステム全体のパフォーマンスを下げることになる。
僕が実務で `INCLUDE` を検討するのは、こんな時だ。
- 特定のクエリの頻度が異常に高い: 「このSQLさえ速ければアプリ全体のレスポンスが改善する」というクリティカルなパスがある場合。
- ソートや範囲検索には使わないカラム: `WHERE` 句には使わないけど、`SELECT` で必ず取得するカラムがある場合。
- テーブルサイズが大きく、ヒープアクセスを減らしたい: テーブルが数百万行を超えてくると、ヒープへのランダムアクセスは大きなコストになる。
—
実際に試してみよう:実行計画の確認
理論だけじゃなく、実際に `EXPLAIN ANALYZE` を叩いて確認するのがエンジニアの流儀だ。
EXPLAIN (ANALYZE, BUFFERS)
SELECT name FROM users WHERE email = ‘taro@example.com’;
`INCLUDE` を使っていない場合、実行計画には `Heap Fetches` という項目が出てくるはずだ。これが「インデックスだけじゃ足りなくて、わざわざテーブルまで見に行った回数」だよ。
`INCLUDE` を導入した後にこの値が `0` になれば、君のチューニングは完璧だ。
—
注意点:銀の弾丸はない
最後に一つだけ釘を刺しておこう。`INCLUDE` したカラムは、あくまで「付随データ」だ。つまり、そのカラムを使ってソートしたり、範囲検索(<や>)をしたりすることはできない。
あくまで「SELECTで取得するだけ」のカラムを、インデックスという「特等席」に座らせてあげるのが `INCLUDE` の役割なんだ。
まとめ
データベースエンジニアの仕事は、魔法を使うことじゃない。「いかにストレージ(ディスク)との会話を減らすか」という、地味な省エネ作業の積み重ねだ。
カバリングインデックスを使いこなせれば、君の書くSQLは一段上のレベルに到達するはず。まずは本番に近い環境で、特定の重いクエリに絞って試してみてくれ。
何か詰まったら、またいつでも聞きに来てよ。現場からは以上だ!
コメント