【PostgreSQL】PostgreSQLのインデックスの種類(B-tree・GiST・GIN・BRIN)

PostgreSQL

こんにちは、かつコーチです。

「インデックスを貼ったのにクエリが速くならない」という相談は、よく聞くと索引の種類がデータの性質に合っていないケースが少なくありません。

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型daterangetsrangeなど、範囲そのものを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_idreserved_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より高くなります。

大量のINSERTUPDATEが発生するテーブルでは、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, 上級者向け, パフォーマンス

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