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でいけるかな?」と考えてみてください。
また何か詰まったら、いつでも聞きに来てくださいね。では!
コメント