cd ~/blog/postgres-index

2026-06-15

PostgreSQL のインデックス種別を図解する

PostgreSQLデータベースインデックスパフォーマンス

インデックスはクエリを高速化するための仕組みですが、PostgreSQL には用途に応じた複数の種類があります。デフォルトで使われる B-Tree 以外にも Hash・GIN・GiST・BRIN があり、データの性質や検索パターンによって使い分けます。

インデックスの役割

インデックスなしでは、PostgreSQL は条件に合う行を全行スキャンする必要があります。インデックスは事前に「目次」を作っておくことで、必要な行だけを素早く見つけられるようにします。

動作
インデックスなし全行スキャン(テーブルサイズに比例して遅くなる)
インデックスありインデックス参照 → 対象行へ直接アクセス

1. B-Tree インデックス

CREATE INDEX のデフォルトです。ほとんどのケースで最初に検討する汎用インデックスです。

構造

平衡木(B+ ツリー)です。ルートから葉まで常に同じ深さになるよう自動的に再編成されます。

各レベルのボックスは区切りキー(separator key)によって決まる値の範囲を表し、一段下のノードへ案内します。

たとえば WHERE id = 43 のとき:

  1. ルートで「43 は 20〜49」→ 内部ノード(20〜49)へ
  2. 内部ノードで「43 は 40〜49」→ 葉ノード(43)へ

全件スキャンせず O(log n) でたどり着けるのが B-Tree の強みです。

葉ノードは連結リストでつながっており、範囲スキャンや ORDER BY でも効率よく動作します。

得意な操作

= < > <= >= BETWEEN IN IS NULL LIKE 'prefix%'(前方一致)ORDER BY

SQL
CREATE INDEX idx_users_email ON users (email);

2. Hash インデックス

ハッシュ関数で値をバケットに振り分ける構造です。

構造

どのバケットに入るかを定数時間 O(1) で求められます。さとううえだ が同じ Bucket 2 に入っているのはハッシュ衝突です。バケット数より値の種類が多いと複数の値が同じバケットに割り当てられることがあり、その場合はバケット内を線形スキャンして一致する値を探します。

得意な操作

=(等値比較のみ)

< > などの範囲比較には使えません。等値検索しか行わないことがはっきりしている列に向いています。

SQL
CREATE INDEX idx_sessions_token ON sessions USING HASH (token);

3. GIN インデックス(Generalized Inverted Index)

配列・JSONB・全文検索のように、1 行に複数の要素が含まれる列を検索するためのインデックスです。

構造

レシピ投稿サイトのテーブルを例に考えます。

idtags
1{ラーメン, 和食}
2{ラーメン, 麺類}
3{和食, 寿司}

テーブルは「行 → 複数のタグ」という方向ですが、GIN はこれを逆にして「タグ → その行 ID のリスト」という対応表を作ります。これが「Inverted(転置)」の意味です。

WHERE tags @> '{ラーメン}' を実行すると、GIN の ラーメン エントリを直接参照して行 1・2 を特定します。配列を全件スキャンせずに済みます。

得意な操作

  • 配列の包含・重なり(@> <@ &&
  • JSONB のキー存在・包含
  • 全文検索(tsvector
SQL
-- 配列列
CREATE INDEX idx_posts_tags ON posts USING GIN (tags);

-- JSONB
CREATE INDEX idx_logs_payload ON logs USING GIN (payload);

-- 全文検索
CREATE INDEX idx_articles_body ON articles USING GIN (to_tsvector('simple', body));

なぜ B-Tree では足りないのか

B-Tree は「1 列 = 1 値」を前提にしています。配列列 tags を B-Tree でインデックスすると、{ラーメン, 和食} という配列全体がキーになります。そのため「ラーメン を含む行を探せ」という検索には使えません。


4. GiST インデックス(Generalized Search Tree)

汎用のツリー構造です。幾何データ・範囲型・全文検索に加え、近傍検索(KNN)にも対応しています。

得意な操作

  • 幾何型: 含む・交差・近傍(KNN)
  • 範囲型: tsrangeint4range の重なり
  • 全文検索: tsvector(GIN より更新が速い)

構造

幾何型の近傍検索

地図上の「新宿駅周辺の店舗を探せ」というクエリを例にします。GiST は地理空間を階層的なバウンディングボックスで管理し、検索エリアと重ならない領域を丸ごとスキップします。

SQL
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);

-- 新宿駅(経度 139.70, 緯度 35.69)に近い順に10件取得
SELECT name FROM locations
ORDER BY geom <-> ST_MakePoint(139.70, 35.69)
LIMIT 10;

範囲型の重なり検索

ホテルの予約テーブルで「2/10〜2/15 に重なる予約を探せ」というクエリを例にします。GiST は内部ノードに配下の全予約を包む最大範囲を持ち、重ならなければ枝ごとスキップします。

SQL
CREATE INDEX idx_reservations_period ON reservations USING GIST (period);

-- 2/10〜2/15 と期間が重なる予約を検索(&& は重なり演算子)
SELECT * FROM reservations
WHERE period && '[2024-02-10, 2024-02-15]'::tsrange;

tsrangeint4range とは

PostgreSQL 組み込みの範囲型です。通常のカラムが「点」の値を持つのに対し、範囲型は「区間」を 1 つの値として扱えます。

  • tsrange — timestamp の範囲(例: '[2024-01-01, 2024-03-31]'
  • int4range — 整数(int)の範囲(例: '[1, 100)'

ホテルの宿泊期間やイベントの開催期間など、区間を 1 列で保持・検索したい場合に使います。

全文検索

GiST の全文検索は各内部ノードにシグネチャ(配下に存在しうる単語の要約)を持ちます。シグネチャに検索語が含まれない枝はスキップします。

内部ノード B のシグネチャに「ラーメン」がないため、配下の記事を一切見ずにスキップします。ただしシグネチャは曖昧な要約のため、実際には該当しない記事(記事2)にも降りてしまうことがあります(偽陽性)。葉ノードで再確認が必要なぶん、GIN より検索が遅くなります。

SQL
CREATE INDEX idx_articles_body ON articles USING GIST (to_tsvector('simple', body));

-- 本文に「ラーメン」を含む記事を検索
SELECT title FROM articles
WHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'ラーメン');

tsvector とは

PostgreSQL の全文検索用データ型です。テキストを形態素解析してキーワードを正規化・重複排除したリストを保持します。

SQL
SELECT to_tsvector('simple', 'ラーメンの作り方を解説します');
-- 'ラーメン' '作り方' '解説'

to_tsvector() でテキストを変換し、GIN または GiST でインデックスを作ることで全文検索が高速になります。

GIN と GiST はどちらも全文検索に使えます。検索速度は GIN が優れ、更新速度は GiST が優れるというトレードオフがあります。


5. BRIN インデックス(Block Range INdex)

物理ブロックの範囲ごとに 最小値・最大値だけ を記録する軽量なインデックスです。

構造

データが時系列順に追記されているテーブルを例にします。

  • Range 1: MAX が 1/31 → 2/15 は範囲外 → スキップ
  • Range 2: MIN 2/1 〜 MAX 2/28 → 2/15 は範囲内 → スキャン
  • Range 3: MIN が 3/1 → 2/15 は範囲外 → スキップ

スキャン対象のブロックを大幅に絞れます。インデックス自体は非常に小さく(テーブルサイズの 0.1% 以下になることもある)、ストレージを圧迫しません。

向いているテーブル

  • 追記のみの大きなログテーブル・イベントテーブル
  • タイムスタンプやシーケンス ID など 物理的な挿入順と値の順が一致する列

物理順と値の順がバラバラな列では効果がありません(min/max の範囲が広くなりすぎてスキップできないため)。

SQL
CREATE INDEX idx_events_created ON events USING BRIN (created_at);

使い分けまとめ

インデックス得意な操作代表的な用途
B-Tree= < > 範囲・ソート汎用(デフォルト)
Hash=(等値のみ)セッショントークン・UUID
GIN包含・全文検索配列・JSONB・tsvector
GiST幾何・範囲・近傍地理座標・日時範囲
BRIN範囲(大テーブル)ログ・時系列の追記テーブル

迷ったら B-Tree から始めるのが基本です。配列や JSONB を検索するなら GIN、地理座標や範囲型なら GiST、億行を超えるログテーブルなら BRIN を検討します。


参照