【テクニカル・上級編】 JSONB用GIN演算子クラス – PostgreSQL

JSONBのGINインデックス、結局どっちを使えばいいんだ?:`jsonb_ops` vs `jsonb_path_ops` の深淵

PostgreSQLでJSONBを使い始めたとき、誰もが一度は「GINインデックス、とりあえず貼っておけばいいや」と考えるはずです。でも、少し規模が大きくなり、クエリが複雑になってくると、`CREATE INDEX … ON table USING GIN (column)` と打つ直前、ふと手が止まることはありませんか?

デフォルトの `jsonb_ops` か、それとも玄人好みの `jsonb_path_ops` か。

今日は、この二つの演算子クラスが裏側で何をしているのか、そしてなぜ「ただ貼る」だけでは不十分なのか、現場の視点から紐解いていきましょう。

—

1. デフォルトの `jsonb_ops` :万能だが、重い

何も指定せずに `USING GIN (data)` と書くと、PostgreSQLは `jsonb_ops` を選択します。この仕組みは非常に直感的です。JSONB内の各キー、各値(文字列、数値、真偽値など)を個別のハッシュ値に変換し、それをエントリとしてGINインデックスに登録します。

  • 何ができるか?:`?`(キーの存在確認)や `?&`、`?|` といった演算子をフルサポートします。
  • 何が起きているか?:JSONBの構造を分解し、「キー+値」のペアをインデックス化します。

ここが落とし穴です。
例えば、`{“tags”: [“postgres”, “optimization”, “performance”]}` というデータがあったとします。`jsonb_ops` は、「tags」というキーと、それぞれのタグの値を個別にインデックスに突っ込みます。これ自体は悪くないのですが、クエリが複雑になり、入れ子の深い構造を検索しようとすると、インデックスエントリが爆発的に増え、インデックスサイズが肥大化します。

2. `jsonb_path_ops` :サイズと速度の「最適解」

一方で、`USING GIN (data jsonb_path_ops)` と指定した場合はどうでしょう。こちらは全くアプローチが異なります。

`jsonb_path_ops` は、JSONBの構造全体をハッシュ化して保存します。つまり、パス(構造)そのものをキーとして扱うため、個々の要素をバラバラに記録しません。

  • 利点1:圧倒的なサイズ効率

構造を圧縮して格納するため、インデックスサイズが劇的に小さくなるケースが多いです。本番環境で数GBの肥大化を経験したことがあるなら、これだけで救われることがあります。

  • 利点2:検索性能の向上

検索時、インデックスは「パス」に対して直接マッチングを行うため、PostgreSQLのエンジンが複数のエントリを走査して突き合わせる(BitmapAnd)コストが削減されます。

ただし、大きな代償があります。
`jsonb_path_ops` は `@>` (包含演算子)のみをサポートします。つまり、`?` 演算子(キーの存在確認)は使えません。このトレードオフを理解していないと、「インデックスはあるのに検索がフルスキャンに落ちる」という悲劇に見舞われます。

—

3. トラブルシューティングの現場から

運用していて、次のような兆候が見えたら要注意です。

  • 「インデックスがあるのに、`EXPLAIN ANALYZE` の結果が Bitmap Heap Scan でインデックスサイズが肥大化している」

→ 典型的な `jsonb_ops` のオーバーヘッドです。`jsonb_path_ops` への移行を検討しましょう。もし `?` 演算子がどうしても必要なら、JSONBの階層を浅くするモデリングの見直しが必要です。

  • 「クエリの実行計画が `Nested Loop` にならず、Bitmap Scan でインデックスが役に立っていない」

→ GINインデックスは、指定した条件が「インデックス可能なもの」かどうかに非常に敏感です。特に `@>` 演算子の右辺に変数や非定数を入れると、プランナがインデックスを無視することがあります。

—

結論:どう使い分けるべきか?

私の経験上、設計指針はこうです。

1. アプリケーションが「構造」を重視する場合(`{“meta”: {“user_id”: 123}}` のような特定のパスを深く検索する):
迷わず `jsonb_path_ops` を選んでください。 検索速度とディスク消費のバランスが最適です。

2. JSONBを「タグ」や「属性の集合」として使う場合(`{“tags”: [“a”, “b”]}` のような、キーや値の存在を `?` で頻繁にチェックする):
`jsonb_ops` が必須です。 サイズは肥大化しますが、ここを妥協してはいけません。

データベースの設計は、結局のところ「何を諦めて、何を得るか」の選択の積み重ねです。GINインデックスという強力なツールを、その特性に合わせて使いこなすこと。それが、数百万行のデータでも軽快に走るシステムを作る、唯一の道だと思っています。

あなたのPostgreSQLが、明日も軽快にクエリを捌くことを祈っています。それでは、また。

コメント

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