関数ベースインデックスの魔術:PostgreSQLにおける「計算のコスト」を劇的に消し去る技術
データベースのパフォーマンスチューニングにおいて、僕たちが最も避けたいのは「フルスキャン」であり、次に避けたいのは「実行時の計算」です。
特に PostgreSQL を扱っていると、`lower(email)` や `jsonb_path_query(data, ‘$.user.id’)` といった式を `WHERE` 句で書くことが日常茶飯事ですよね。でも、ちょっと立ち止まって考えてみてください。そのクエリ、実行されるたびに全行の式を計算していませんか?
今回は、PostgreSQL の「関数ベースインデックス(Expression Index)」について、表面的な使い方ではなく、エンジニアとして知っておくべき内部構造と、現場でハマりやすい罠について深く掘り下げてみたいと思います。
—
関数ベースインデックスの正体:B-Treeの中身は何を保持しているのか
関数ベースインデックスを定義する際、私たちは `CREATE INDEX idx_user_email ON users (lower(email));` のように書きます。
ここでのポイントは、「PostgreSQL は計算結果をインデックスのキーとして保存している」という事実です。インデックスのリーフページには、`email` という列の値が入っているわけではありません。その関数を適用した結果がバイナリデータとして格納されています。
つまり、インデックスの実体は通常のB-Treeと何ら変わりません。クエリが走った瞬間、オプティマイザは「あ、この `lower(email)` という式、インデックスのキーと合致するな」と判断し、クエリプランナーが計算済みのインデックスを参照するように仕向けるわけです。
ここで重要なのは、クエリ側の記述とインデックス側の式が「完全に一致」していなければならないという点です。たとえ `lower(email)` と `LOWER(email)` であっても、式が違えばインデックスは使われません。この厳密さが、現場での「インデックスが効かない!」というトラブルの第一原因になります。
—
パフォーマンストラブルの現場から:見えない計算コスト
よくある落とし穴をいくつか共有します。
1. 関数自体のコストと「Volatility(変動性)」
PostgreSQL の関数には `IMMUTABLE`, `STABLE`, `VOLATILE` という属性がありますよね。
関数ベースインデックスを作成できるのは原則として `IMMUTABLE` な関数だけです。なぜか? それは、インデックス作成時に計算された値が、後から変わってしまったらインデックスが壊れる(論理矛盾を起こす)からです。
もし、あなたが自作の関数をインデックスに組み込むなら、その関数が本当に `IMMUTABLE` かどうか、内部で外部テーブルを参照していないか、慎重に確認してください。ここを誤魔化すと、データの更新時に奇妙な不整合に悩まされることになります。
2. JSONB インデックスの沼
最近だと `jsonb_extract_path_text` をインデックスにするケースが多いはずです。ここで注意したいのは、「巨大なJSONへのインデックスは、インデックスサイズを肥大化させる」という点です。
インデックスのサイズが大きくなりすぎると、メモリ(`work_mem` や `shared_buffers`)に乗り切らなくなり、結果としてディスクI/Oがボトルネックになります。`jsonb` に対してインデックスを張る際は、`GIN` インデックス(`jsonb_path_ops` など)を使うべきか、特定のフィールドだけを狙い撃ちした関数ベースインデックスで十分か、トレードオフを常に見極める必要があります。
—
デバッグの勘所:オプティマイザに語らせる
「なぜインデックスが使われないのか?」と悩んだら、迷わず `EXPLAIN (ANALYZE, BUFFERS)` を叩いてください。
もし実行計画で `Seq Scan` が出ていて、かつ `WHERE` 句にインデックスと同じ式があるなら、十中八九、以下のいずれかです。
- データ型の不一致: 式の結果の型が、インデックスを定義した時の型と微妙に違う(例えば `text` と `varchar` の違いでキャストが発生している)。
- 照合順序(Collation)の問題: データベース全体の照合順序と、インデックス作成時の照合順序が異なっていると、インデックスは使われません。これ、意外と盲点なんです。
—
最後に:エンジニアとしての矜持
関数ベースインデックスは非常に強力なツールです。しかし、魔法ではありません。
「とりあえず何でもインデックスを張れば速くなる」という考えは捨てましょう。インデックスは書き込み(`INSERT`/`UPDATE`/`DELETE`)のたびに更新コストを伴います。式が複雑であればあるほど、その計算コストは書き込み時のオーバーヘッドとして跳ね返ってきます。
僕が設計する際は、常にこう自問します。「この計算コストを、検索時に払わせるか、書き込み時に払わせるか」。
パフォーマンスチューニングの正解は、教科書にはありません。あなたのプロダクトのクエリパターンと、データの特性という「現場の事実」の中にだけあります。ぜひ、ご自身の DB で `EXPLAIN` のプランをじっくり眺めてみてください。そこには、PostgreSQL がどのようにデータを解釈しているかという、静かな熱い物語が刻まれていますから。
コメント