【実務・中級編】 GINインデックス – PostgreSQL

GINインデックスを使いこなせ:PostgreSQLで「JSONBや配列」を爆速検索する技術

どうも。最近、若手エンジニアから「PostgreSQLのJSONBカラムを検索したらクエリが遅くて……」という相談をよく受けます。

テーブルの特定カラムに「タグの配列」や「複雑な設定値のJSONB」を突っ込む設計、便利ですよね。でも、いざデータが数百万件を超えたとき、普通に検索をかけるとフルスキャンが走ってしまい、DBが悲鳴を上げる。そんな経験、ありませんか?

今回は、そんな時に絶対的な救世主となる「GIN(Generalized Inverted Index:汎用転置インデックス)」について、実務的な視点で深掘りしていこうと思います。

—

そもそも、なぜ普通のB-treeじゃダメなのか?

PostgreSQLで一番馴染み深い「B-treeインデックス」は、値の大小関係を並べて保持するのに長けています。`WHERE id = 10` や `WHERE created_at > ‘2023-01-01’` なんて検索には最強です。

しかし、JSONBの中にある特定のキーや、配列の中に含まれる一つの要素を探したい場合、B-treeは無力です。「ある一つの値の中に、複数の要素が内包されている」というデータ構造に対して、B-treeは全データを走査するしか方法がないからです。

そこで登場するのが、転置インデックス(Inverted Index)の仕組みを持つGINです。

GINインデックスの仕組みを「本」で例えると

転置インデックスを理解する一番手っ取り早い方法は、本の「索引」を想像することです。

  • 通常のデータ(B-tree): 本を最初から最後までめくって、該当するキーワードを探す。
  • GINインデックス: 巻末の「索引」を見て、そのキーワードが何ページにあるかを確認する。

GINは、データの中身を分解(トークン化)して、「どのキーが」「どのレコードにあるか」という対応表を裏で作っています。だから、JSONBや配列の中にある小さな要素を、一瞬で探し出せるわけです。

—

実践:GINインデックスを貼ってみよう

例えば、ECサイトのプロダクト管理テーブルで、こんなJSONBカラムがあるとしましょう。

— 商品情報テーブル
CREATE TABLE products (
id serial PRIMARY KEY,
attributes jsonb
);

— こんなデータが入っている
— {“color”: “red”, “tags”: [“sale”, “new”, “import”]}

この `attributes` 内の `tags` に `sale` が含まれる商品を検索したい場合、普通に書くとこうなります。

SELECT FROM products WHERE attributes @> ‘{“tags”: [“sale”]}’;

このクエリを爆速にするには、こうします。

CREATE INDEX idx_products_attributes ON products USING GIN (attributes);

たったこれだけ。これだけで、PostgreSQLは `attributes` カラムの中身を解析し、含まれる要素一つひとつをインデックスに登録してくれます。実行計画を `EXPLAIN ANALYZE` で見れば、Sequential ScanからBitmap Index Scanに切り替わっているのが確認できるはずです。

—

ここが現場の落とし穴!気をつけるべき「デメリット」

GINインデックスは強力ですが、魔法の杖ではありません。現場で注意すべきポイントが2つあります。

1. 更新コストがとにかく高い

B-treeはデータ更新時にそのノードだけ書き換えればいいのですが、GINは違います。一つのレコードを更新すると、その中に含まれる「要素」すべてをインデックス側で再計算・更新しなければなりません。
「書き込み頻度が高いテーブル」にむやみにGINを貼ると、更新処理でDBのCPUが張り付くことになります。

2. インデックスサイズが肥大化する

転置インデックスは、値が増えれば増えるほどインデックスのサイズも大きくなります。ストレージ容量には余裕を持ってください。

—

先輩からのアドバイス:もっと速くしたいなら

もし、JSONB全体ではなく「特定のキー」しか検索しないのであれば、「JSONB_PATH_OPS」という演算クラスを使うのが定石です。

CREATE INDEX idx_products_tags
ON products USING GIN (attributes jsonb_path_ops);

デフォルトのGINよりもインデックスサイズが小さくなり、検索速度も向上します。ただし、`@>` 演算子での検索に特化するので、柔軟性は少し落ちます。状況に合わせて使い分けるのが「できるエンジニア」のやり方です。

—

まとめ:使いどころを見極める

GINインデックスは、「読み取り専用に近い大きなJSONBデータ」や「複雑な属性検索が必要なシステム」には最高の相棒です。逆に、頻繁にINSERT/UPDATEが走るログテーブルのような場所には慎重に導入してください。

PostgreSQLは奥が深いですが、その分、理解すればするほど強力な武器になります。まずは今担当しているプロダクトのクエリを見直して、フルスキャンしている場所があれば「GINでいけるかな?」と考えてみてください。

また何か詰まったら、いつでも聞きに来てくださいね。では!

コメント

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