【テクニカル・上級編】 JSONBインデックス戦略 – PostgreSQL

JSONBを飼い慣らす:PostgreSQLのインデックス戦略における「深淵」

PostgreSQLの `jsonb` 型に出会ったとき、多くのエンジニアは「これでスキーマレスの自由が手に入る」と小躍りします。しかし、実務で数千万行のデータを扱うようになると、その「自由」がインデックス設計という名の「重い代償」を伴うことに気づくはずです。

今日は、ありきたりなGINインデックスの解説は飛ばして、もう少し深い、アーキテクチャの核心に触れる話をしましょう。

—

1. デフォルトのGINインデックスという「妥協」

まず、皆さんがよく使う `CREATE INDEX ON table USING GIN (data);` について。
これ、実は非常に強力ですが、インデックスサイズが肥大化しがちです。内部的には、JSON内のすべてのキーと値が個別にトークン化され、ツリーに登録されます。

ここで意識すべきは、「デフォルトの演算子クラス(`jsonb_ops`)は、存在確認(`?`, `?&`, `?|`)と包含演算子(`@>`)の両方をサポートするために、冗長なエントリを生成している」という事実です。もし、あなたが特定のパスに対する検索しか行わないのであれば、デフォルト設定はただの「無駄なメモリ消費」になりかねません。

—

2. `jsonb_path_ops` という「最適解」

もし検索パターンが `{ “user”: { “id”: 123 } }` のような、包含演算子(`@>`)によるものに限定されているなら、迷わず `jsonb_path_ops` を使ってください。

CREATE INDEX idx_data_path ON table USING GIN (data jsonb_path_ops);

なぜこれが速いのか?
`jsonb_ops` がキーと値のペアをバラバラに格納するのに対し、`jsonb_path_ops` はハッシュ値を用いて「パス全体」の構造をインデックス化します。

  • メリット: インデックスサイズが劇的に小さくなり、ルックアップ速度が向上します。
  • トレードオフ: 存在確認演算子(`?`など)が使えなくなります。

「自由」を少し手放すだけで、クエリプランナは遥かに効率的なプランを選択できるようになります。これこそが、アーキテクチャを理解したエンジニアの戦い方です。

—

3. 「部分インデックス」で無駄を削ぎ落とす

さて、次はパフォーマンスのボトルネックを物理的に叩く話です。
JSONB全体にインデックスを貼るのではなく、特定のキーにのみインデックスを当てる手法はご存知でしょうか?

CREATE INDEX idx_user_status ON table ((data->>’status’))
WHERE (data ? ‘status’);

ここで重要なのは、`WHERE` 句による部分インデックスです。
JSONBには「ある行にはキーが存在するが、別の行には存在しない」という疎なデータ構造が頻発します。この `WHERE` 句を付与することで、インデックスのサイズは劇的に縮小します。

パフォーマンストラブルシューティングの視点:
もし、特定のクエリが遅いとアラートが鳴ったとき、`EXPLAIN ANALYZE` を見てください。インデックススキャンが「Bitmap Heap Scan」になっている場合、インデックスが大きすぎてメモリに乗り切っていない可能性があります。その時、この「部分インデックス」は劇薬のように効きます。

—

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

僕が後輩によく言うのは、「インデックスはデータの地図だ」ということです。地図があまりに詳細すぎれば、それ自体を持ち歩くのが困難になります。

1. まずは `jsonb_path_ops` で無駄を削る。
2. それでも重いなら、クエリを絞り込んで部分インデックスを検討する。
3. それでも解決しないなら、JSONBの構造自体を正規化する(RDBの原則に立ち返る)。

PostgreSQLは非常に懐の深いデータベースです。しかし、そのポテンシャルを使い切るには、オプティマイザが「何を考えているか」を想像する力が必要です。

皆さんのデータベースに、今日から少しだけ「最適化のメス」を入れてみてはいかがでしょうか? きっと、ディスクIOの改善という形で、システムが静かに感謝してくれるはずです。

それでは、また次回の深掘りでお会いしましょう。

コメント

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