【テクニカル・上級編】 複合インデックス (Composite Index) – PostgreSQL

複合インデックスの「順序」という名の迷宮を読み解く

PostgreSQLを長く触っていると、ふとした瞬間に「なぜこのクエリはこれほどまでに遅いのか?」という壁にぶつかることがあります。`EXPLAIN ANALYZE` を叩くと、インデックスは使われているはずなのに、期待したほどのパフォーマンスが出ない。

そんな時、多くのエンジニアが真っ先に疑うのが「複合インデックスの順序」です。今日は、教科書的な説明を一段掘り下げて、PostgreSQLの内部アーキテクチャからこの「順序問題」を解剖してみましょう。

B-treeの構造が強制する「左端」の掟

まずは基本の確認ですが、PostgreSQLのB-treeインデックスは、複合キーをひとつの値として辞書順(lexicographical order)に並べ替えています。ここで重要なのは、「左側の列(Leading Column)がソートの主軸である」という事実です。

例えば `(a, b)` というインデックスを作ったとき、インデックスツリーの中では `a` が先行してソートされ、その中で `b` がソートされます。

  • `WHERE a = 1 AND b = 2`:完璧です。ツリーを辿ってピンポイントで値を特定できます。
  • `WHERE a = 1`:これも問題ありません。`a` は左端にいるので、B-treeの恩恵をフルに受けられます。
  • `WHERE b = 2`:ここが罠です。 `b` は `a` の値が確定して初めてソート順が保証されるため、このクエリではインデックス全体をスキャンする「Index Scan」あるいは「Parallel Sequential Scan」にフォールバックしてしまいます。

「カーディナリティ」だけを信じてはいけない

よくある誤解が「カーディナリティ(値の多様性)が高い列を左に置くべき」という格言です。確かに一理ありますが、それはあくまで「等価比較(`=`)」がメインの場合の話です。

もしクエリが以下のような形だったらどうでしょう?

SELECT FROM orders WHERE status = ‘shipped’ AND created_at > ‘2023-01-01’;

`status` のカーディナリティは低く、`created_at` は高い。しかし、もし `status` が頻繁にクエリで使われる固定条件なら、`(status, created_at)` とインデックスを張るのが正解です。

理由はシンプルで、PostgreSQLのクエリプランナは、左端の列が等価比較されている限り、その次の列の範囲検索(Range Scan)へスムーズに移行できるからです。「検索の絞り込み条件(Filter)として頻繁に登場するか」という視点が、カーディナリティの呪縛から私たちを解放してくれます。

内部構造から見る「インデックスの肥大化」と「オーバーヘッド」

複合インデックスを設計する際、忘れてはならないのが「メンテナンスコスト」です。

複合インデックスは、列を増やせば増やすほど、インデックスのページサイズは大きくなり、書き込み時のオーバーヘッドが増大します。特に更新頻度の高いテーブルでは、不要な列を含めた複合インデックスは、VACUUMの負荷を増大させ、ホットスタンバイのレプリケーション遅延を引き起こす引き金にもなります。

私が現場でよく行うチューニングは、「Covering Index(INCLUDE句の活用)」です。

PostgreSQL 11以降であれば、`CREATE INDEX … INCLUDE (col_name)` を使うことで、インデックスの検索キーには含めずに、インデックスの末端(リーフノード)にデータだけを保持させることができます。これにより、インデックスの肥大化を抑えつつ、テーブル本体へのヒープアクセスを回避する「Index Only Scan」を狙うことができます。

トラブルシューティングの勘所

もし本番環境で「インデックスが効いていない」と感じたら、まずは以下の3点を確認してみてください。

1. データ型の不一致: `WHERE col_a = ‘123’` とクエリを投げていて、`col_a` が `integer` 型の場合、暗黙の型変換が発生してインデックスが無視されることがあります。
2. 統計情報の鮮度: `ANALYZE` が適切に走っていないと、プランナは「インデックスを使うよりフルスキャンの方が速い」と誤った見積もりをします。`pg_stats` を覗いてみてください。
3. 相関(Correlation): 物理的なテーブルの並びとインデックスの並びが全く違う場合、`Random Access` が多発し、Index Scanが遅くなります。`CLUSTER` コマンドで物理順序を最適化する検討が必要かもしれません。

最後に:インデックスは「地図」である

インデックスは、データベースという広大な森を歩くための地図です。地図が詳しすぎれば(複合インデックスが長すぎれば)、地図そのものを読むのに時間がかかり、地図が簡素すぎれば、目的の場所に辿り着くために森を彷徨うことになります。

「これさえ貼れば速くなる」という魔法のインデックスは存在しません。クエリの傾向を観測し、プランナの思考をトレースし、そして何より、そのインデックスがシステムの成長と共にどう変化するかを想像すること。

それが、私たちエンジニアに求められる「データベースへの敬意」なのだと思います。皆さんのクエリが、今日も効率的なプランで実行されることを願っています。

コメント

タイトルとURLをコピーしました