PostgreSQLのJSONB検索を高速化するGINインデックスの正しい貼り方と注意点
PostgreSQL Database Performance SQL
結論
包含検索(@>)特化なら jsonb_path_ops を指定してGINを作成し、書き込みスパイク事故防止には gin_pending_list_limit をチューニングします。
-- PostgreSQL公式仕様:jsonb_path_ops インデックスの作成と包含クエリ
CREATE INDEX idx_users_data_path ON users USING gin (data jsonb_path_ops);
-- GINインデックスが適用される検索
SELECT id, data FROM users WHERE data @> '{"status": "active"}';
PostgreSQL公式仕様:2つの演算子クラス比較
PostgreSQL公式ドキュメント(Section 8.14 / 65.4)に基づき、GIN(Generalized Inverted Index)の2種類の演算子クラスのトレードオフを整理します。
| 項目 | jsonb_ops (デフォルト) | jsonb_path_ops |
|---|---|---|
| サポートする演算子 | @>, @?, @@, ?, `? | , ?&` |
| インデックス構造 | キー・値を個別項目として全生成 | パス全体の32bitハッシュ値を1項目生成 |
| インデックスサイズ | 大 | 小(サイズが約半減し検索速度最速) |
| 空オブジェクトの扱い | インデックス化される | {"a": {}} 等の値無しハッシュはインデックス非生成 |
実際に起こるトラブル:fastupdate による突然のCPU100%スパイク事故
Reddit(r/postgresql)や本番運用で頻繁に報告される有名なトラブルとして、GINインデックス付きテーブルへのINSERT/UPDATEが突然数十秒フリーズし、CPU使用率が100%に跳ね上がる現象があります。
原因:Pending List の同期マージ(クリーンアップ)
GINインデックスはデフォルトで FASTUPDATE = ON に設定されています。
書き込み要求を一度 pending list(メモリ上のバッファ、デフォルト上限 4MB)に溜め込み、その上限(gin_pending_list_limit)を超えた瞬間に、クエリをロックしてメインのGINインデックスへ同期マージ(クリーンアップ)を開始するためレイテンシスキャンが発生します。
トラブル回避・チューニング手順
- 検索が
@>中心であればjsonb_path_opsでインデックスサイズ自体を小さく抑える - 書き込み頻度の高いテーブルでは、バッファ上限を拡大して同期マージの頻度を下げる
- 突然のスパイクを許容できず一定のレイテンシを保ちたい場合は
FASTUPDATEをOFFにする
-- 対策1: pending list 上限を 4MB から 128MB へ拡大してマージ頻度を下げる
ALTER INDEX idx_users_data_path SET (gin_pending_list_limit = '128MB');
-- 対策2: スパイクを排除し、書き込みごとに分散処理させる(FASTUPDATE無効化)
ALTER INDEX idx_users_data_path SET (fastupdate = OFF);
失敗境界
- 存在演算子
?を使いたい場合
data ? 'email'(特定キーが存在するか)という検索を行う場合、jsonb_path_opsではインデックスがスキップされてSeq Scanに落ちます。この場合はjsonb_opsを指定する必要があります。