PostgreSQL: PostgreSQL インデックスの内部構造と最適化

最終更新:2026-08-26

1. 学習内容


2. ストーリー

BobはEコマースプラットフォームのDBAです。商品検索ページがどんどん遅くなり、JSONBカラムのフルスキャンでクエリに3秒もかかるようになったとユーザーから報告がありました。

BobがJSONBカラムにGINインデックスを追加すると、検索は300ミリ秒まで短縮されました。さらに、アクティブユーザー向けのクエリも遅いことに気づきましたが、90%のユーザーは既に非アクティブ化されており、テーブル全体にインデックスを作ると容量を無駄にします。そこでstatus = 'active'の行だけを対象とする部分インデックスを使用し、インデックスサイズを80%削減してクエリを高速化しました。


3. 概念: インデックス種類の概要

(1) 6種類のインデックス

インデックス種類 正式名称 適したデータ型 代表的なユースケース
B-Tree バランス木 ソート可能な全型 等価検索、範囲検索、ソート、前方一致LIKE
Hash ハッシュテーブル 全型 単純な等価検索
GIN 汎用転置インデックス 配列、JSONB、全文検索 包含検索、全文検索
GiST 汎用検索木 ジオメトリ、範囲、全文 空間検索、最近傍検索
BRIN ブロック範囲インデックス 物理順序のある大規模テーブル 時系列データ、時間範囲スキャン
SP-GiST 空間分割GiST 不均衡な分割構造 電話番号、ルーティング

▶ サンプル: テーブルの既存インデックスを確認する

SQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'orders';
TEXT 📖 参照専用
 indexname              | indexdef
------------------------+--------------------------------------------------
 orders_pkey            | CREATE UNIQUE INDEX ... ON orders USING btree (order_id)
 idx_orders_customer_id | CREATE INDEX ... ON orders USING btree (customer_id)

(2) インデックス種類の選択判断

100%
flowchart TD
    A{クエリの種類は?} -->|等価/範囲/ソート| B[B-Tree]
    A -->|単純な等価| C[Hash]
    A -->|配列/JSONBの包含| D[GIN]
    A -->|空間/ジオメトリ/最近傍| E[GiST]
    A -->|大規模テーブルの逐次スキャン| F[BRIN]
    A -->|不均衡な分割構造| G[SP-GiST]

    B --> B1["デフォルトのインデックス種類<br/>90%のケースで最適"]
    C --> C1["使用頻度は低い<br/>通常はB-Treeの方が優れる"]
    D --> D1["JSONB @> ?<br/>配列 @> <br/>tsvector @@ "]
    E --> E1["PostGIS<br/>範囲の重なり"]
    F --> F1["1000万行以上の時系列<br/>サイズが非常に小さい"]
    G --> G1["電話番号プレフィックス<br/>IPルーティング"]

    style A fill:#e1f5fe
    style B fill:#c8e6c9
    style D fill:#fff9c4
    style F fill:#fff9c4

4. 概念: B-Treeインデックス

(1) B-Treeの特徴とユースケース

B-TreeはPostgreSQLのデフォルトインデックス種類で、等価検索、範囲検索、ソート、IS NULL、前方一致LIKE検索をサポートします。

サポートされる演算子
等価 WHERE col = 100
範囲 WHERE col > 100 AND col < 200
ソート ORDER BY col
IS NULL WHERE col IS NULL
前方一致LIKE WHERE col LIKE 'abc%'
BETWEEN WHERE col BETWEEN 1 AND 10
サポートされない演算子 理由
LIKE '%abc' 先頭ワイルドカードはB-Treeの順序を利用できない
col::text = '100' 型の不一致。式インデックスが必要
LOWER(col) = 'abc' 関数の結果はインデックス化されない。式インデッ��スが必要

▶ サンプル: B-Treeインデックスを作成する

SQL
CREATE INDEX idx_orders_amount ON orders (amount);

CREATE INDEX idx_orders_date_amount ON orders (order_date, amount DESC);

Output:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: UNIQUEインデックス

SQL
CREATE UNIQUE INDEX idx_users_email ON users (email);

Output:

TEXT 📖 参照専用
CREATE TABLE

UNIQUEインデックスはデータの一意性保証とクエリパフォーマンスの両方を提供します。


5. 概念: GINインデックス

(1) GINインデックスの原理

GIN(汎用転置インデックス)は転置インデックスで、要素からそれを含む行へのマッピングです。「含む」系の検索に適しています。

演算子
JSONB @> ? `? ?&`
配列 @> <@ && WHERE tags @> ARRAY['sale']
tsvector @@ WHERE body @@ to_tsquery('postgres')

▶ サンプル: JSONBカラムへのGINインデックス

SQL
CREATE INDEX idx_products_attrs ON products USING GIN (attrs);

SELECT product_id, name
FROM products
WHERE attrs @> '{"category": "electronics"}';
TEXT 📖 参照専用
 product_id |    name
------------+------------
        101 | Laptop Pro
        205 | Smart Watch

▶ サンプル: 配列カラムへのGINインデックス

SQL
CREATE INDEX idx_products_tags ON products USING GIN (tags);

SELECT product_id, name
FROM products
WHERE tags @> ARRAY['summer', 'sale'];

Output:

TEXT 📖 参照専用
  result  
----------
   42.50
(1 row)

▶ サンプル: 全文検索用のGINインデック��

SQL
CREATE INDEX idx_articles_body ON articles USING GIN (to_tsvector('english', body));

SELECT id, title
FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('postgresql & index');

Output:

TEXT 📖 参照専用
CREATE TABLE

(2) GINとB-Treeの比較

観点 B-Tree GIN
検索種類 等価 / 範囲 包含 / 検索
書き込み速度 速い 遅い(転置リストの更新が必要)
インデックスサイズ 中程度 大きめ
適した型 スカラー 配列 / JSONB / 全文
ソート対応 あり なし

6. 概念: GiST / BRIN / SP-GiST / Hash

(1) GiSTインデックス

GiSTは汎用検索木フレームワークで、カスタム分割戦略をサポートします。代表的な用途は空間データです。

ユースケース 演算子 拡張機能
ジオメトリデータ && @ <@ PostGIS
範囲型 && @> <@ 組み込み
全文検索 @@ 組み込み

▶ サンプル: 範囲型へのGiSTインデックス

SQL
CREATE INDEX idx_events_time_range ON events USING GiST (time_range);

SELECT event_id, title
FROM events
WHERE time_range && daterange('2025-01-01', '2025-03-01');

Output:

TEXT 📖 参照専用
CREATE TABLE

(2) BRINインデックス

BRIN(ブロック範囲インデックス)は各データブロ��クのサマリー情報(最小値/最大値)を保存します。非常にコンパクトで、物理的に順序付けられた大規模テーブルに適しています。

観点 B-Tree BRIN
インデックスサイズ 大きい 非常に小さい(約1/1000)
精度 正確 近似(余分なブロックをスキャンする可能性あり)
メンテナンスコスト 高い 非常に低い
ユースケース ランダム検索 時系列の範囲スキャン

▶ サンプル: 時系列テーブルへのBRINインデックス

SQL
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at)
  WITH (pages_per_range = 32);

SELECT count(*) FROM logs
WHERE created_at BETWEEN '2025-06-01' AND '2025-06-30';

Output:

TEXT 📖 参照専用
 count 
-------
     5
(1 row)

(3) SP-GiSTインデックス

SP-GiSTは電話番号プレフィックスやIPルーティングのような不均衡な分割構造に適しています。

▶ サンプル: 電話番号プレフィックスへのSP-GiSTインデックス

SQL
-- 最初にbtree_gist拡張機能を有効化: CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE INDEX idx_customers_phone ON customers USING SP-GiST (phone prefix_range);

Output:

TEXT 📖 参照専用
CREATE TABLE

(4) Hashインデックス

Hashインデックスは単純な等価検索のみをサポートし、範囲検索やソートはサポートしません。PostgreSQL 10以前はHashインデックスにWALの問題がありましたが(現在は修正済み)、通常はB-Treeの方が良い選択です。

観点 B-Tree Hash
等価検索 速い 速い
範囲検索 サポート 非サポート
ソート サポート 非サポート
WAL 完全 PostgreSQL 10以降完全
推奨 デフォルト選択 使用頻度は低い

7. 概念: 高度なインデックス機能

(1) 部分インデックス

部分インデックスはWHERE条件を満たす行のみを含み、インデックスサイズとメンテナンスコストを削減します。これはPostgreSQLの特長機能です。

観点 完全インデックス 部分インデックス
含まれる行 全行 条件に合致する行のみ
インデックスサイズ 大きい 小さい
メンテナンスコスト 書き込みごとに更新 該当行のみ更新
ユースケース 一般的な検索 サブセットのみを対象とする検索

▶ サンプル: アクティブユーザーのみにインデックス

SQL
CREATE INDEX idx_users_active_email ON users (email)
WHERE status = 'active';

Output:

TEXT 📖 参照専用
CREATE TABLE
SQL
SELECT email FROM users WHERE status = 'active' AND email = 'alice@example.com';

このクエリは部分インデックスを使用します。status = 'active'条件がないクエリは使用しません。

▶ サンプル: 未発送の注文のみにインデックス

SQL
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE shipped = false;

Output:

TEXT 📖 参照専用
CREATE TABLE

(2) 式インデックス

クエリ条件がカラムを関数や計算でラップしている場合、通常のインデックスは使用できません。式インデックスは計算結果に対してインデックスを構築します。

▶ サンプル: 大文字小文字を区別しない検索

SQL
CREATE INDEX idx_users_email_lower ON users (LOWER(email));

SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';

Output:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: 日付切り捨て検索

SQL
CREATE INDEX idx_orders_date_trunc ON orders (DATE_TRUNC('day', created_at));

SELECT COUNT(*) FROM orders
WHERE DATE_TRUNC('day', created_at) = '2025-06-15'::date;

Output:

TEXT 📖 参照専用
 count 
-------
     5
(1 row)

(3) 複合インデックスと左端プレフィックス

複合インデックス(a, b, c)が対応できるクエリパターン:

検索条件 使用されるか 理由
WHERE a = 1 はい 左端プレフィックス
WHERE a = 1 AND b = 2 はい 左端プレフィックス
WHERE a = 1 AND b = 2 AND c = 3 はい 完全一致
WHERE b = 2 いいえ 左端カラムがない
WHERE b = 2 AND c = 3 いいえ 左端カラムがない
WHERE a = 1 AND c = 3 部分的 カラムaのみ使用。cは連続していない

▶ サンプル: 複合インデックスを作成する

SQL
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC);

Output:

TEXT 📖 参照専用
CREATE TABLE

(4) CONCURRENTLYによるオンラインインデックス構築

デフォルトではインデックス構築時に排他ロックを取得し、書き込みをブロックします。CONCURRENTLYは書き込みをブロックしませんが、構築が遅くなります。

方式 ロック 書き込みブロック 速度 トランザクション内可?
CREATE INDEX 排他ロック ブロックする 速い はい
CREATE INDEX CONCURRENTLY 共有ロック ブロックしない 遅い いいえ

▶ サンプル: オンラインでインデックスを構築する

SQL
CREATE INDEX CONCURRENTLY idx_orders_region
ON orders (region);

Output:

TEXT 📖 参照専用
CREATE TABLE
⚠️ 注意: CONCURRENTLYはトランザクションブロック内で実行できません。


8. 概念: EXPLAIN実行計画

(1) EXPLAINの基本

コマンド 説明 クエリ実行?
EXPLAIN 実行計画を表示 いいえ
EXPLAIN ANALYZE 実行して実測値を表示 はい
EXPLAIN BUFFERS バッファヒットを表示 はい
EXPLAIN (FORMAT JSON) JSON形式で出力 いいえ

▶ サンプル: 実行計画を表示する

SQL
EXPLAIN
SELECT * FROM orders WHERE customer_id = 1;
TEXT 📖 参照専用
                              QUERY PLAN
----------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders  (cost=0.29..8.31 rows=1 width=72)
   Index Cond: (customer_id = 1)

▶ サンプル: EXPLAIN ANALYZE

SQL
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 1;
TEXT 📖 参照専用
                              QUERY PLAN
----------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders
   (cost=0.29..8.31 rows=1 width=72) (actual time=0.015..0.016 rows=2 loops=1)
   Index Cond: (customer_id = 1)
 Planning Time: 0.085 ms
 Execution Time: 0.032 ms

(2) 主要なスキャン種類

スキャン種類 意味 インデックス使用?
Seq Scan テーブル全体の逐次スキャン いいえ
Index Scan インデック��スキャン(テーブル行を取得) はい
Index Only Scan インデックスのみスキャン(テーブル参照な��) はい(カバリングインデックス)
Bitmap Scan ビットマップスキャン(大量バッチ) 部分的
Parallel Seq Scan 並列テーブル全体スキャン いいえ

▶ サンプル: Index Only Scan

SQL
EXPLAIN
SELECT customer_id FROM orders WHERE customer_id = 1;
TEXT 📖 参照専用
 Index Only Scan using idx_orders_customer_id on orders
   Index Cond: (customer_id = 1)

インデックスのみを読み取り、データ行のテーブル参照が不要なため、最高のパフォーマンスが得られます。


9. インデックスのメンテナンスと失敗

(1) REINDEXによる再構築

多数の挿入/削除を繰り返すと、インデックスが肥大化することがあります。REINDEXで再構築して容量を回収します。

方式 説明 ロック
REINDEX INDEX idx 単一インデックスを再構築 排他ロック
REINDEX TABLE tbl テーブル上の全インデックスを再構築 排他ロック
REINDEX INDEX CONCURRENTLY idx オンライン再構築(PG 12+) ブロックしない

▶ サンプル: インデックスを再構築する

SQL
REINDEX INDEX idx_orders_customer_id;

REINDEX INDEX CONCURRENTLY idx_orders_customer_id;

Output:

TEXT 📖 参照専用
-- SQL statement executed successfully

(2) インデックスが使われない一般的なケース

ケース 修正方法
カラムを関数でラップ WHERE LOWER(col) = 'x' 式インデックス
暗黙的な型変換 WHERE varchar_col = 123 型を統一する
先頭ワイルドカード WHERE col LIKE '%abc' GIN / pg_trgm
OR条件 WHERE a=1 OR b=2 個別インデックスまたはUNION
統計情報が古い 大量データ変更後 ANALYZE
左端プレフィックス違反 複合インデックス(a,b)をbで検索 インデックスまたはクエリを調整
部分インデックス条件不一致 WHERE status='active'で全件検索 WHERE削除または完全インデックス構築

▶ サンプル: 暗黙的な型不一致でインデックスが失敗する

SQL
SELECT * FROM users WHERE phone = 13800138000;

SELECT * FROM users WHERE phone = '13800138000';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

phoneはVARCHARです。1行目の暗黙的な変換によりインデックスが使われず、2行目はインデックスを使用します。


10. 総合サンプル

Bobのインデックス最適化計画 — JSONB商品検索 + アクティブユーザー部分インデックス + 複合インデックス + BRIN時系列インデックス:

SQL
CREATE INDEX CONCURRENTLY idx_products_attrs_gin
  ON products USING GIN (attrs);

CREATE INDEX CONCURRENTLY idx_users_active_email
  ON users (email, last_login_at)
  WHERE status = 'active';

CREATE INDEX CONCURRENTLY idx_orders_customer_date
  ON orders (customer_id, order_date DESC);

CREATE INDEX CONCURRENTLY idx_audit_log_created_brin
  ON audit_log USING BRIN (created_at)
  WITH (pages_per_range = 32);

EXPLAIN ANALYZE
SELECT p.product_id, p.name, p.attrs
FROM products p
WHERE p.attrs @> '{"category": "electronics", "in_stock": true}';

EXPLAIN ANALYZE
SELECT user_id, email
FROM users
WHERE status = 'active'
  AND email LIKE 'alice%'
ORDER BY last_login_at DESC
LIMIT 10;

11. 実行フロー

インデックス種類の選択と最適化フロー:

100%
flowchart TD
    A["遅いクエリを発見"] --> B["EXPLAIN ANALYZE"]
    B --> C{"スキャン種類は?"}
    C -->|"Seq Scan"| D{"適切なインデックスがあるか?"}
    C -->|"Index Scan"| E["インデックス使用中<br/>クエリ/インデックスを最適化"]
    D -->|"なし"| F{"データ型は?"}
    D -->|"あり但しミス"| G["インデックス無効化を確認"]
    F -->|"スカラー/ソート"| H["B-Tree作成"]
    F -->|"JSONB/配列"| I["GIN作成"]
    F -->|"ジオメトリ/範囲"| J["GiST作成"]
    F -->|"大規模テーブル逐次"| K["BRIN作成"]
    F -->|"サブセットのみ"| L["部分インデックス作成"]
    F -->|"関数/計算"| M["式インデックス作成"]
    H --> N["CONCURRENTLYでデプロイ"]
    I --> N
    J --> N
    K --> N
    L --> N
    M --> N

    style B fill:#e1f5fe
    style G fill:#ffccbc
    style N fill:#c8e6c9

❓ よくある質問

Q 「インデックスは多ければ多いほど良い」のでしょうか?
A いいえ。インデックスが増えるたびに書き込みのオーバーヘッド(INSERT/UPDATE/DELETE時にインデックスのメンテナンスが必要)とストレージ容量が増加します。高頻度クエリにのみインデックスを作成し、定期的にpg_stat_user_indexesで使用状況を確認してください。
Q CONCURRENTLYでの構築が失敗した場合はどうなりますか?
A 失敗したCONCURRENTLY構築はINVALIDなインデックスを残します。SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%INVALID%'または\d+ tblで見つけ、DROP INDEXで削除してください。
Q LIKE検索がインデックスを使わないのはなぜですか?
A LIKE 'abc%'はB-Treeを使用できますが、LIKE '%abc'のような先頭ワイルドカードは使用できません。GINとpg_trgm拡張機能が必要です。
Q BRINインデックスに適したケースは?
A 物理的に順序付けられた大規模テーブル(時間順に追記されるログテーブルなど)です。データが物理順序と無関係な場合、BRINの近似フィルタリングは効果が低く推奨されません。
Q 部分インデックスは書き込みパフォーマンスに影響しますか?
A 完全インデックスより影響は少ないです。WHERE条件を満たさない行では、書き込み時に部分インデックスの更新が不要なため、I/Oを節約できます。
Q 複合インデックス(a, b)はWHERE a=1 ORDER BY bをサポートしますか?
A はい。複合インデックスはaでソート後bでソートされているため、WHEREフィルタとORDER BYの両方を満たし、余分なソートを回避できます。

📖 まとめ


📝 練習問題

  1. ordersテーブルのcustomer_idカラムにB-Treeインデックスを作成し、EXPLAINでクエリがインデックスを使用することを確認してください。
  2. productsテーブルのJSONBカラムにGINインデックスを作成し、@>クエリの実行計画の変化を観察してください。
  3. ⭐⭐ 部分インデックスを作成: status = 'pending'の注文のcreated_atカラムのみにインデックスを作成し、完全インデックスとのサイズ差を比較してください。
  4. ⭐⭐ LOWER(email)に式インデックスを作成し、大文字小文字を区別しない検索がインデックスを使用することを確認。次に複合インデックス(region, created_at DESC)を作成し、地域と時間でソートされたクエリをサポートしてください。
  5. ⭐⭐⭐ 数千万行のログテーブルに対して、BRINインデックス + B-Tree複合インデックス + 部分インデックスの組み合わせ案を設計し、EXPLAIN ANALYZEで最適化前後のクエリ時間を比較してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%