PostgreSQLの「複合インデックス」でハマらないための鉄則:順序がすべてを決める
やあ。最近、コードレビューをしていて「あ、ここはインデックスの張り方を少し変えるだけで、クエリの実行速度が数倍から数十倍変わるのにな」と思うことがよくあるんだ。
PostgreSQLを触っていると、インデックスは「とりあえず貼れば速くなる」と思われがちだけど、実は魔法じゃない。特に「複合インデックス(複数のカラムを組み合わせたインデックス)」は、設計次第で救世主にもなれば、ただのストレージの無駄遣いにもなる。
今日は、現場で後輩によく話している「複合インデックスを設計する時のコツ」を、少し噛み砕いて解説するよ。
—
1. なぜ「カラムの順序」が命なのか?
まず大前提として、PostgreSQLのB-treeインデックスは、「左端一致(Leftmost Prefix)」というルールで動いている。
例えば、`users`テーブルに対して `(status, created_at)` という複合インデックスを貼ったとするよね。このとき、PostgreSQLはインデックスを「まず`status`でソートし、その中で`created_at`をソートする」という順序で保持しているんだ。
これがどういう意味を持つかというと:
- 効くクエリ: `WHERE status = ‘active’ AND created_at > ‘2023-01-01’`
- `status`で絞り込んでから`created_at`を見るから、爆速。
- 効くクエリ: `WHERE status = ‘active’`
- 左端の`status`が使われているから、これでもインデックスは効く。
- 効かない(効きにくい)クエリ: `WHERE created_at > ‘2023-01-01’`
- ここが重要! 左端の`status`がないと、インデックスの「最初の入り口」が見つからないから、フルスキャン(全件走査)に近い動きになってしまうんだ。
つまり、「頻繁に検索条件として使うカラム」を、インデックスの左側に持ってくるのが鉄則。これが逆転していると、インデックスの恩恵をほとんど受けられないこともあるんだ。
—
2. 実践的な設計例:どっちを左にすべき?
よくある「会員一覧画面」のクエリを例に考えてみよう。
SELECT FROM users
WHERE status = ‘active’
AND category_id = 5
ORDER BY created_at DESC;
さて、ここで `(status, category_id, created_at)` という複合インデックスを貼るべきか、それとも `(category_id, status, created_at)` がいいか。判断基準は「カーディナリティ(値の多様性)」と「等価比較か範囲比較か」だ。
迷ったらこの基準で選べ
1. 等価比較(=)を左へ: `status = ‘active’` のように「=」で絞り込めるカラムを左に置く。範囲指定(`>`や`<`など)を左に置くと、それ以降の条件がインデックスで絞り込まれにくくなるからだ。 2. 選択性が高いものを左へ: 全体のレコードに対して、その値でどれくらい絞り込めるか。例えば「性別(男/女)」よりも「会員ID」の方が絞り込めるよね。絞り込みの強いカラムを左に置くと、インデックスの探索範囲がグッと小さくなる。
今回の例なら、`status`(値が少ない)よりも `category_id`(値が多い)の方が絞り込み効果が高いことが多い。だから、`(category_id, status, created_at)` とした方が、統計情報的にも効率が良いケースが多いんだ。
—
3. 「複合インデックス」vs「単一インデックス」の迷い
「単一インデックスを3つ貼るのと、複合インデックスを1つ貼るのと、どっちがいいの?」という質問もよく受ける。
結論から言うと、「検索条件を組み合わせて使うことが多いなら、間違いなく複合インデックス」だ。
単一インデックスを複数貼っても、PostgreSQLは「Bitmap Index Scan」を使って複数のインデックスをマージしようとするんだけど、これにはCPUコストがかかるし、結局データ本体へのアクセス(Heap Fetch)回数が増えて遅くなることが多い。
ただし、注意点もある:
複合インデックスは、それ単体でサイズが肥大化しやすい。更新頻度が高いテーブルに巨大な複合インデックスを貼りまくると、今度は`INSERT`や`UPDATE`のたびにインデックスの更新コストが発生して、書き込みが重くなる。
- 読み取り重視のテーブル: 複合インデックスを積極的に活用する。
- 書き込み重視のテーブル: インデックスは必要最小限に絞る。
このバランスを忘れないでほしい。
—
先輩からのアドバイス:まずはEXPLAINしてみよう
最後になるけど、理屈で考えるのも大事だけど、最後は必ず `EXPLAIN (ANALYZE, BUFFERS)` を見てほしい。
EXPLAIN (ANALYZE, BUFFERS)
SELECT FROM users WHERE status = ‘active’ AND category_id = 5;
ここで `Index Scan` が使われているか、それとも `Seq Scan` になっているか。`Buffers` の数字が大きすぎていないか。これらを確認する習慣をつけるだけで、君のSQLチューニングレベルは一気に上がるはずだよ。
インデックス設計に「これが絶対の正解」というものはない。データ量やアクセスの偏りによっても変わるからね。まずは手を動かして、PostgreSQLがどう動いているのかを覗いてみることから始めてみて。
また何か詰まったら、いつでも聞きに来てよ。エンジニア同士、一緒に成長していこう。
コメント