こんにちは、かつコーチです。
「インデックスを貼ったのにクエリが速くならない」という相談は、よく聞くと索引の種類がデータの性質に合っていないケースが少なくありません。
PostgreSQLはCREATE INDEX ... USINGで複数のインデックスアクセスメソッドを選べます。
今回はB-tree・GiST・GIN・BRINの4つを中心に、それぞれ何に向いているかを整理します。
インデックス自体の基本(CREATE INDEXの基礎構文)は理解している前提で進めます。
B-tree:デフォルトかつ最も汎用的
向いているデータ・演算子
CREATE INDEXで種類を省略すると自動的にB-treeになります。
等値比較(=)と範囲比較(<、>、BETWEEN)、ORDER BYのソートに強く、9割のケースはB-treeで事足ります。
CREATE INDEX idx_orders_ordered_at ON orders (ordered_at);
-- 範囲検索・ソートどちらもB-treeが効く
SELECT * FROM orders
WHERE ordered_at >= '2026-08-01' AND ordered_at < '2026-09-01'
ORDER BY ordered_at;
複数列の複合インデックスを作る場合、先頭列から順に絞り込みに使われる点も押さえておきましょう。
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
-- customer_idだけの条件でもこのインデックスは使える
SELECT * FROM orders WHERE customer_id = 42;
-- statusだけの条件では(先頭列customer_idがないため)このインデックスは使われにくい
SELECT * FROM orders WHERE status = 'completed';
GiST:範囲・空間データの重なり判定
向いているデータ・演算子
GiST(Generalized Search Tree)は、「重なっているか」「近いか」といった、等値比較では表現できない条件を扱えるインデックスです。
range型(daterangeやtsrangeなど、範囲そのものを1つの値として持つデータ型)や、PostGISの空間データとの相性がよく知られています。
予約システムで「期間が重複する予約がないか」をチェックする場面が典型例です。
CREATE TABLE room_reservations (
reservation_id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
reserved_period TSRANGE NOT NULL,
EXCLUDE USING GIST (room_id WITH =, reserved_period WITH &&)
);
EXCLUDE USING GISTは、GiSTインデックスを使って「同じroom_idでreserved_periodが重なる(&&演算子)行の挿入を拒否する」という排他制約です。
UNIQUE制約が等値の重複しか防げないのに対し、GiSTを使った排他制約は「範囲の重なり」という条件で重複を防げます。
-- 重なる期間の予約はエラーになる
INSERT INTO room_reservations (room_id, reserved_period)
VALUES (101, '[2026-09-10, 2026-09-12)');
INSERT INTO room_reservations (room_id, reserved_period)
VALUES (101, '[2026-09-11, 2026-09-13)');
-- ERROR: conflicting key value violates exclusion constraint
私が会議室予約システムを構築した際、アプリケーション側で重複チェックのロジックを書いていましたが、同時アクセスによる競合を完全には防げませんでした。
EXCLUDE USING GISTに置き換えてからは、DB側で確実に重複を弾けるようになり、アプリ側の防御コードを削れました。
GIN:配列・JSONB・全文検索
向いているデータ・演算子
GIN(Generalized Inverted Index)は、1つの列の中に複数の値が入っているデータ(配列、JSONB、全文検索用のtsvector)に強いインデックスです。
「この配列に特定の値が含まれるか」「このJSONBにこのキーがあるか」といった包含関係の検索を高速化します。
CREATE TABLE articles (
article_id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT[] NOT NULL,
metadata JSONB NOT NULL
);
CREATE INDEX idx_articles_tags ON articles USING GIN (tags);
CREATE INDEX idx_articles_metadata ON articles USING GIN (metadata);
-- 配列の包含検索
SELECT * FROM articles WHERE tags @> ARRAY['PostgreSQL'];
-- JSONBのキー存在検索
SELECT * FROM articles WHERE metadata @> '{"status": "published"}';
JSONBとGINインデックスの詳しい使い方は、以前の記事「JSONB型とGINインデックスで半構造化データを扱う」で扱っているので、そちらも参考にしてください。
GINインデックスの書き込みコストに注意
GINインデックスは検索は高速ですが、書き込み時のインデックス更新コストがB-treeより高くなります。
大量のINSERT・UPDATEが発生するテーブルでは、fastupdateオプション(更新をいったんペンディングリストに溜め、まとめて反映する仕組み)の挙動も踏まえてベンチマークを取ることをおすすめします。
BRIN:巨大なログ・時系列テーブル
向いているデータ・演算子
BRIN(Block Range Index)は、テーブルの物理的な並び順とデータの値がおおむね相関しているカラムに使う、非常に軽量なインデックスです。
B-treeが1行1行の位置を細かく記録するのに対し、BRINはページ(複数行の塊)単位で最小値・最大値だけを記録します。
インデックスサイズがB-treeの数十分の1〜数百分の1になる代わりに、絞り込みの精度は粗くなります。
時系列で追記され続けるログテーブルのように、「挿入順とタイムスタンプ順がほぼ一致している」データに向いています。
CREATE TABLE access_logs (
log_id BIGSERIAL PRIMARY KEY,
accessed_at TIMESTAMPTZ NOT NULL,
path TEXT NOT NULL
);
CREATE INDEX idx_access_logs_accessed_at ON access_logs USING BRIN (accessed_at);
数億行規模のログテーブルにB-treeを貼るとインデックスだけで数十GBになることがありますが、BRINなら数MB程度に収まるケースも珍しくありません。
ただし、データの挿入順とインデックス対象列の値が相関していない(バッチ処理で古いデータを後から大量に挿入するなど)場合は、BRINの絞り込み精度が大きく落ちるため注意してください。
4種類の使い分け早見表
| 種類 | 得意な条件 | 典型的な用途 | 書き込みコスト |
|---|---|---|---|
| B-tree | =、<、>、ORDER BY | 主キー、日時範囲検索、一般的な検索条件 | 低 |
| GiST | 範囲の重なり、近傍検索 | 予約の重複防止、地理空間データ | 中 |
| GIN | 配列・JSONBの包含、全文検索 | タグ検索、JSONB検索、全文検索 | 高 |
| BRIN | 物理順と相関する範囲検索 | 巨大な時系列ログ | 非常に低い |
よくあるつまずきポイント・エラー対処
Before/After:GINが使えるはずの条件でインデックスが使われない
JSONBに対してGINインデックスを貼っても、演算子が合っていないとインデックスが使われません。
-- ❌ Before:@>ではなく->>で値を取り出して比較すると、
-- デフォルトのGINインデックスは使われにくい
SELECT * FROM articles WHERE metadata ->> 'status' = 'published';
-- ✅ After:包含演算子@>を使うか、->>で使う式に合わせて式インデックスを作る
SELECT * FROM articles WHERE metadata @> '{"status": "published"}';
-- どうしても->>の形で検索したい場合は式インデックスを作成する
CREATE INDEX idx_articles_metadata_status ON articles ((metadata ->> 'status'));
インデックスの種類だけでなく「どの演算子・どの式で検索するか」までインデックス定義と一致していないと、実行計画がインデックスを選んでくれません。
この判定は次回のEXPLAIN ANALYZEで実際に確認する方法を解説します。
まとめ
この記事のポイント
- B-treeは等値・範囲検索・ソートに強く、大半のケースをカバーする汎用インデックス
- GiSTは範囲の重なりや空間データの近傍判定に強く、排他制約とも組み合わせられる
- GINは配列・JSONB・全文検索の包含関係に強いが、書き込みコストは高め
- BRINは物理順とデータが相関する巨大な時系列テーブルで極めて軽量に機能する
- インデックスの種類だけでなく、検索に使う演算子・式が一致しているかも重要
次に読むべき記事
インデックスの種類を理解したら、実際にそのインデックスが使われているかを確認するEXPLAIN ANALYZEの読み方に進みましょう。
- シーケンスとSERIAL・IDENTITY列の違い
- EXPLAIN ANALYZEで実行計画を読む(近日公開)
タグ: PostgreSQL, 上級者向け, パフォーマンス