GIN(Generalized Inverted Index)は、配列・JSONB・全文検索用のtsvectorのように「1つの値の中に複数の要素が入っているデータ」を対象にした転置索引です。B-treeが行の値そのものを並べるのに対し、GINは値から要素を取り出してキーにし、そのキーを含む行を逆引きします。この記事では、PostgreSQL 18.6(2026年8月13日リリース)のマニュアルとソースを一次情報として、構造・作成方法・GiSTとの使い分け・fastupdateの運用・制限までを実装目線で整理します。
まとめ:GINインデックスの要点
- GINは(キー, ポスティングリスト)の組を持つ転置索引で、同じキーは一度しか格納されないため、同じ値が何度も現れるデータに対して索引がコンパクトになります。
- 組み込みの演算子クラスは array_ops・jsonb_ops・jsonb_path_ops・tsvector_ops の4つで、jsonb列の既定は jsonb_ops です。
- GINは1行ずつ返す索引スキャンを実装していない(amgettuple が NULL)ため、実行計画は Bitmap Index Scan と Bitmap Heap Scan の組み合わせに固定されます。ソート済み出力もIndex Only ScanもUNIQUE索引も使えません(複数列索引は可能)。
- 更新の重さは fastupdate(既定ON)とペンディングリストで緩和されますが、リストが膨らむと検索が遅くなります。応答時間の安定を優先するならOFFにします。
- PostgreSQL 18では、GIN索引の並列作成と、amcheckの gin_index_check() による破損検査が追加されました。
GINインデックスの構造とBitmap Index Scanになる理由
GINは、索引対象の値(マニュアルでいう item)からキーを抽出し、(キー, ポスティングリスト)の組を格納します。ポスティングリストは、そのキーが出現する行ID(TID)の集合です。キー値は一度しか格納されないため、「東京」というタグが10万行に付いていても、キー自体は索引内に1つだけ存在します。
ポスティングリストとポスティングツリーの切り替わり
内部はキーに対するB-treeで、リーフページの各タプルは2つの形を取ります。行IDのリストが小さくキー値と一緒に1つの索引タプルに収まるうちは単純なポスティングリストとして持ち、収まらなくなるとヒープポインタのB-tree、すなわちポスティングツリーへのポインタに切り替わります。頻出語のように行数が膨れるキーでも索引が破綻しないのはこの二段構えのためです。なお複数列のGIN索引は、(列番号, キー値)の複合値に対する単一のB-treeとして実装されます。
実行計画がBitmap Index Scanに固定される仕組み
GINは索引エントリに元の値の一部しか持たないため、マニュアルはIndex Only Scanを支持できないと明記しています。実装側も同じで、PostgreSQL 18系のソース(src/backend/access/gin/ginutil.c)ではGINのアクセスメソッドは amgettuple が NULL、amgetbitmap のみが実装され、amcanreturn も NULL です。つまりGINは「1行ずつ順に返す索引スキャン」を提供できず、候補行のビットマップを作ってからヒープを読む形にしかなりません。複数キーのAND条件も突き合わせはGIN内部(gingetbitmap と各演算子クラスの consistent 関数)で解決されるため、計画上は BitmapAnd が並ぶのではなく Bitmap Index Scan 1本として現れます。計画の読み方はPostgreSQLの実行計画の読み方|EXPLAIN ANALYZEと見積もり乖離の診断で扱っています。
データ型別の作成方法と演算子クラス
作成構文は通常のCREATE INDEXに USING GIN を付けるだけで、対象データ型に応じて演算子クラスを選びます。組み込みは array_ops(&&、@>、<@、=)、jsonb_ops(@>、@?、@@、?、?|、?&)、jsonb_path_ops(@>、@?、@@)、tsvector_ops(@@)の4つです。拡張が提供する演算子クラスもあり、hstore は @>、?、?&、?| に対するGIN索引を、btree_gin は int4・text・timestamp などにB-tree相当の振る舞いを与えます。
-- 配列(array_ops が既定)
CREATE INDEX idx_tags ON users USING GIN (tags);
-- JSONB(既定は jsonb_ops、演算子を絞って小さくするなら jsonb_path_ops)
CREATE INDEX idx_jdoc ON api USING GIN (jdoc);
CREATE INDEX idx_jdoc_path ON api USING GIN (jdoc jsonb_path_ops);
-- 全文検索(tsvector_ops)
CREATE INDEX idx_body ON docs USING GIN (to_tsvector('english', body));
-- LIKE の部分一致(pg_trgm 拡張が必要)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_name_trgm ON items USING GIN (name gin_trgm_ops);
配列とJSONBで使える演算子の差
jsonb列の既定は jsonb_ops です。jsonb_path_ops はキー存在演算子(?、?|、?&)を扱えない代わりに、対応する演算子については性能が良いとマニュアルが述べています。実務で引っかかりやすいのは、索引可能な演算子であっても索引列に直接適用されていなければ使われない点です。jdoc列にGINを張っても jdoc -> 'tags' ? 'qui' は索引を使えず、CREATE INDEX ON api USING GIN ((jdoc -> 'tags')) のような式インデックスにするか、jdoc @> '{"tags": ["qui"]}' の包含形に書き換える必要があります。JSONB列を正規化列に切り出す判断はPostgreSQL JSONBの使い方|json型との格納差・GIN索引と正規化列の設計境界に分けています。
全文検索(tsvector_ops)での前提
マニュアルは全文検索の索引としてGINを推奨しています。GINはtsvectorの語彙素(lexeme)だけを格納し、重みラベルは保持しません。そのため重み付きのtsqueryで検索すると、索引で絞った後にテーブル行の再チェックが発生します。転置索引という発想そのものを比較したい場合はElasticsearchとは?転置インデックスとシャード設計・ライセンス系列で決める採用可否も合わせて読むと、DB内で完結させるか検索エンジンを立てるかの線引きがしやすくなります。
LIKE部分一致をGINで拾うpg_trgm
pg_trgm の gin_trgm_ops を使うと、LIKE・ILIKE・~・~*・= に対してトライグラム単位の索引検索が効きます。前後にワイルドカードを置く LIKE '%foo%' でも索引が使える点が実務上の利点です。ただし距離演算子(<->、<<-> など)による近傍順の取得はGiSTでは効率的に実装できてもGINではできません。類似度検索の閾値は pg_trgm.similarity_threshold で、既定値は0.3です。なおトライグラムは英数字の3文字組を単位にするため、日本語の短い語句には向きません。日本語の部分一致を索引で捌くなら、2-gram索引を作る pg_bigm や、Groongaを索引に使う PGroonga といった拡張が受け皿になります。
GiST・B-treeとの使い分け
PostgreSQLの索引はB-tree・Hash・GiST・SP-GiST・GIN・BRINの6種類(ほかに拡張のbloom)で、GINと競合するのは主にGiSTです。全文検索では両方が使えます。GiSTは各文書を固定長シグネチャで表す非可逆な索引で、ビットが衝突すると偽陽性が出るため、ヒープを読んで確認する処理が増えます。シグネチャ長 siglen の既定値は演算子クラスごとに違い、全文検索(tsvector)のGiSTは124バイト、pg_trgm の gist_trgm_ops は12バイトで、いずれも最大2024バイトです。一方でGiSTはINCLUDE句によるカバリング索引にでき、近傍順の取得にも向きます。
| 観点 | GIN | GiST | B-tree |
|---|---|---|---|
| 対象 | 配列/JSONB/tsvector/トライグラム | tsvector/幾何/範囲型 | スカラー値の比較 |
| 偽陽性 | なし(重み付き検索時は再チェック) | あり(シグネチャ衝突) | なし |
| Index Only Scan | 不可 | 演算子クラス次第で可 | 常に可 |
| UNIQUE索引 | 不可 | 不可 | 可 |
| ソート済み出力 | 不可 | 不可 | 可 |
| 近傍順の取得 | 不可 | 可 | 対象外 |
「GINの検索はGiSTの約3倍速く、構築は約3倍遅く、更新は約10倍遅い」という数値を引く記事が今も多く見られます。この一覧はPostgreSQL 9.4までのマニュアルに載っていたもので、9.5以降の該当ページからは削除されており、18.6のマニュアルにもありません。10年以上前の版に由来する比率なので、選定の根拠にするなら自分のデータで pg_relation_size と EXPLAIN ANALYZE を取り直してください。
fastupdateとペンディングリストの運用
GINは1行の挿入で複数のキーが増えるため、更新が重くなりがちです。これを緩和するのが fastupdate で、既定はONです。新しいエントリはいったん未整列のペンディングリストに入り、VACUUMまたはautoanalyze時、gin_clean_pending_list() の呼び出し時、あるいはリストが gin_pending_list_limit(既定4MB)を超えたときに、本体側へ一括で移されます。
代償は検索側にあります。検索はペンディングリストも走査するため、リストが大きいほど遅くなります。さらに、リストが上限を超えた瞬間の更新は前景でクリーンアップを走らせるので、その1回だけ極端に遅くなります。応答時間のばらつきを避けたいシステムでは fastupdate をOFFにするのが素直です。注意点として、ALTER INDEXでOFFにしても既存のペンディングエントリは自動で吐き出されません。
-- 溜まり具合を測る(pgstattuple 拡張)
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstatginindex('idx_body'); -- version, pending_pages, pending_tuples
-- 索引単位で上限を変える(格納パラメータはキロバイト単位の整数)
ALTER INDEX idx_body SET (gin_pending_list_limit = 32768);
-- 応答時間を優先して無効化し、残りを明示的に整理する
ALTER INDEX idx_body SET (fastupdate = off);
SELECT gin_clean_pending_list('idx_body');
ここで間違えやすいのが単位です。サーバ変数の gin_pending_list_limit は 32MB のように単位付きで書けますが、索引の格納パラメータ側はキロバイト単位の整数しか受け付けません。32MBと書くと invalid value for integer option で弾かれます。溜まり具合そのものは pgstatginindex() の pending_pages と pending_tuples で観測できます。
クリーンアップを前景に出さない現実的な運用は、autovacuumを対象テーブルに十分効かせることです。しきい値の考え方はPostgreSQLのVACUUM運用|autovacuumのしきい値設計とXID周回・肥大の切り分けにまとめています。
GINで踏む制限とチューニングの勘所
- Index Only Scanは使えません。索引エントリが元の値の一部しか持たないためで、カバリング索引にしたい要件とは相性が悪くなります。
- UNIQUE索引は作れません。マニュアルは「一意索引をサポートするのは現状B-treeのみ」と明記しています。複数列索引はGINでも作成できますが、スカラー列と組み合わせるには btree_gin の演算子クラスが要ります。GIN対象の列とB-tree対象の列を同時に条件にするクエリでは、索引を2本作ってBitmapANDさせるより複数列GIN1本のほうが効率的な場合がある、とマニュアルは述べています。
- 大量投入の前は索引を落とす。1行あたり多数のキーが挿入されるため、バルクロードでは索引を削除してから作り直すほうが速く済みます。fastupdateが有効ならペナルティは小さくなりますが、規模が大きいときは作り直しが有利です。
- 作成時は maintenance_work_mem を惜しまない。GINの構築時間はこの設定に強く依存するとマニュアルが名指ししています。
- 結果が多すぎる全文検索には gin_fuzzy_search_limit。返す行数のソフト上限で、既定は0(無制限)です。マニュアルは経験則として5000〜20000あたりを挙げています。設定すると結果は全体からランダムに選ばれた部分集合になるため、件数の正確さが要る画面には使えません。
PostgreSQL 18で変わったGIN周りと、GINを選ばない判断
PostgreSQL 18では、GIN索引を並列で作成できるようになりました(Tomas Vondra、Matthias van de Meent)。GINの構築はB-treeより時間がかかる部類なので、大きなテーブルの初期構築やREINDEXの所要時間に直接効きます。ワーカーを増やしたいときに触るのは max_parallel_maintenance_workers ですが、並列ワーカーは maintenance_work_mem のうち1プロセスあたり32MB以上の取り分を必要とするため、メモリ側も一緒に上げないと要求どおりのワーカー数になりません。加えてamcheckに gin_index_check() が追加され、GIN索引の整合性をSQLから検査できるようになりました。障害調査で「索引が壊れているのか統計が古いだけなのか」を切り分けたい場面で使えます。なお2026年9月4日時点の現行版は18.6(2026年8月13日リリース)で、19系はベータ段階です。
-- 索引定義とサイズの確認
SELECT indexdef FROM pg_indexes WHERE indexname = 'idx_body';
SELECT pg_size_pretty(pg_relation_size('idx_body'));
-- PostgreSQL 18で追加されたGIN索引の整合性検査
CREATE EXTENSION IF NOT EXISTS amcheck;
SELECT gin_index_check('idx_body');
逆に、GINを選ぶべきでない条件もはっきりしています。第一に、等価比較や範囲検索しか使わない列です。この用途はB-treeのほうが速く、Index Only Scanも一意制約も使えます。pg_trgmのマニュアル自身、等価演算子については通常のB-tree索引ほど効率的でない場合があると断っています。第二に、更新が絶え間なく走るうえに応答時間のばらつきも許されない列です。fastupdateをONにすれば検索がペンディングリストの走査で遅くなり、OFFにすれば書き込みが重くなるという交換条件からは逃げられません。第三に、検索スコア順の並べ替えを索引側で解決したい場合です。GINは順序付き出力を返せないため、ランキング用途では拡張のRUM(postgrespro/rum)やGiSTを検討することになります。
よくある質問
GINインデックスとは何の略で、どう呼びますか?
Generalized Inverted Index(汎用転置インデックス)の略です。マニュアルの65.4節に定義があり、複合的な値の中の要素を検索するケース向けに設計された索引だと説明されています。公式に定められたカタカナ表記はなく、日本語の文書でも英字のままGINと書かれるのが通例です。
GINインデックスが使われているかはどう確認しますか?
EXPLAINで実行計画を見ます。GINが使われていれば Bitmap Index Scan on 索引名 と、その上位に Bitmap Heap Scan が現れます。Index Scan や Index Only Scan と表示されることはGINでは起こりません。
jsonb_ops と jsonb_path_ops はどちらを選ぶべきですか?
キー存在演算子(?、?|、?&)を使うなら既定の jsonb_ops しか選べません。検索が包含(@>)やjsonpath(@?、@@)に限られるなら jsonb_path_ops のほうが対応演算子は少ない代わりに性能が良いとマニュアルは述べています。
GINインデックスでUNIQUE制約は作れますか?
作れません。CREATE INDEXのマニュアルに「一意索引をサポートするのは現状B-treeのみ」と書かれています。一意性が必要な列は別途B-treeの一意索引を張ってください。
GINインデックスの更新が遅いときは何から見ますか?
まず fastupdate の状態とペンディングリストの溜まり方です。gin_pending_list_limit(既定4MB)を超えた更新は前景でクリーンアップを走らせるため、その1回だけ突出して遅くなります。autovacuumを効かせて背景で整理させるか、応答時間を優先するなら fastupdate をOFFにして gin_clean_pending_list() で明示的に整理します。