インデックスのライフサイクル(本番運用視点)
本番運用中の PostgreSQL でインデックスを操作する場面は、想像以上に多く発生します。
未使用インデックスの削除、肥大化した GIN の再構築、CREATE INDEX CONCURRENTLY の途中失敗の片付け、不要 partition の整理…
本ドキュメントは「すでにデータが入っているテーブルに対して、安全にインデックス操作するための知見」を、実測データつきでまとめたものです。
本ドキュメントの実測値は、idp-server プロジェクトの
libs/idp-server-database/postgresql/operation/drop-unused-gin-indexes/benchmark/RESULTS.mdで取得した数値を基にしています。 検証環境: PostgreSQL 15 / shared_buffers 1 GB / partitioned table 95-125 個 /security_eventテーブル (B-tree 12 個 + GIN 1 個)。
目次
基礎編 — 用語と仕組みを揃える
応用編 — 本番運用での実践
- CREATE INDEX
- DROP INDEX
- REINDEX – 削除しない選択肢
- 安全な戦略: lock_timeout + retry パターン
- スケーリング特性 (実測)
- DROP 時間の変動要因
- 周辺ファクター
- 本番運用チェックリスト
まとめ編 — 押さえても陥る落とし穴と実例
1. インデックスのライフサイクル
インデックスは静的な存在ではなく、テーブル同様にライフサイクルを持ちます。
┌─────────────────────────────────────────────────────────────────┐
│ インデックスのライフサイクル │
│ │
│ ┌──────────┐ │
│ │ CREATE │ ← テーブル作成時 / マイグレーション │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ 使用 (planner が選択) │ │
│ │ - SELECT で参照される │ │
│ │ - INSERT/UPDATE で維持 │ │
│ │ - VACUUM で整理 │ │
│ └────┬─────────────────────┘ │
│ │ │
│ │ サイズ肥大化 / 断片化 / 統計の劣化 │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ REINDEX │ │ DROP │ │ ALTER │ │
│ │ (再構築) │ │ (削除) │ │ (移行) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ 本番運用中に発生する操作 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.1 各操作の特徴
| 操作 | いつ実行する | 主な目的 |
|---|---|---|
CREATE INDEX | 設計時、性能改善時 | 新規追加 |
CREATE INDEX CONCURRENTLY | 本番稼働中の追加 | 書き込みを止めずに追加 |
REINDEX | 肥大化・断片化対応 | サイズ縮小、断片化解消 |
REINDEX CONCURRENTLY | 本番稼働中の再構築 | 書き込みを止めずに再構築 |
DROP INDEX | 未使用・不要 | 削除 |
DROP INDEX CONCURRENTLY | 本番稼働中の削除 | 書き込みを止めずに削除 |
ALTER INDEX | リネーム、tablespace 移動 | メタ情報変更 |
1.2 ライフサイクルで注意すべき本質
「テーブルがあるのにインデックスがない状態」は性能上不利:
- 通常時は
CREATE INDEXしてから運用に投入する - 本番運用後に追加するなら
CONCURRENTLYでロックを最小化する - 削除や再構築も同様
「インデックスのサイズはデータと比例して増えない」:
- B-tree は概ね線形に増える
- GIN は **データの構造(JSONB のキー数等)**にも依存
- 肥大化したらサイズ縮小手段は
REINDEXまたはDROP+CREATE
2. ロックの基礎
CREATE/DROP/REINDEX で発生するロックの理解は必須です。
ロックの一般論は dev-04-transactions.md §4 ロック機構 を参照。 内部の LWLock (ProcArrayLock 等) については dba-10-procarraylock-internals.md を参照。 本ドキュメントでは インデックス操作で発生するロック に絞って解説します。
2.1 PostgreSQL のテーブルロック階層
PostgreSQL のテーブルレベルロックは 8 段階あります:
弱 ────────────────────────────────────────────────── 強
ACCESS ROW ROW SHARE SHARE SHARE EXCLUSIVE ACCESS
SHARE SHARE EXCLUSIVE UPDATE ROW EXCLUSIVE
EXCL EXCL
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
SELECT SELECT INSERT VACUUM CREATE CREATE REFRESH DROP
FOR UPDATE ANALYZE INDEX TRIGGER MATVIEW TABLE
UPDATE DELETE CONC. 一部 CONC. DROP IDX
ALTER ALTER
conflict matrix
主要なロックの互換性:
AS RS RE SUE SH SRE EX AE
ACCESS SHARE o o o o o o o ✗
ROW SHARE o o o o o o ✗ ✗
ROW EXCLUSIVE o o o o ✗ ✗ ✗ ✗
SHARE UPDATE o o o ✗ ✗ ✗ ✗ ✗
EXCLUSIVE
SHARE o o ✗ ✗ o ✗ ✗ ✗
SHARE ROW o o ✗ ✗ ✗ ✗ ✗ ✗
EXCLUSIVE
EXCLUSIVE o ✗ ✗ ✗ ✗ ✗ ✗ ✗
ACCESS ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗
EXCLUSIVE
2.2 重要なロックモード
ACCESS EXCLUSIVE — 最強の排他ロック
- 取る操作:
DROP TABLE,DROP INDEX(非 CONCURRENTLY),TRUNCATE,VACUUM FULL,REINDEX(非 CONCURRENTLY),ALTER TABLE(一部) - 特徴: 他の すべてのロックと非互換 (
SELECTすらブロック) - 本番リスク: これが取得待ち状態に入ると、後続全クエリが queue で待たされる
ROW EXCLUSIVE — INSERT/UPDATE/DELETE が取る
- 取る操作:
INSERT,UPDATE,DELETE - 特徴: 自分同士は互換 (複数 INSERT が並行可能)
- 重要:
ACCESS EXCLUSIVEと非互換 → DROP/CREATE 系と詰まる
SHARE UPDATE EXCLUSIVE — CONCURRENTLY のキー
- 取る操作:
CREATE INDEX CONCURRENTLY,DROP INDEX CONCURRENTLY,REINDEX CONCURRENTLY,VACUUM(非 FULL),ANALYZE - 特徴:
INSERT/UPDATE/DELETE(ROW EXCLUSIVE) と 互換 ★ - これが CONCURRENTLY の魔法: 書き込みを止めずに DDL ができる
2.3 ロック取得待ちのメカニズム
時系列:
T=0 SELECT A 実行中 (ACCESS SHARE 取得)
T=1 DROP INDEX 投入
→ ACCESS EXCLUSIVE 要求
→ A と非互換 → 待機キューに入る
T=2 SELECT B 到着
→ ACCESS SHARE 要求
→ ★ DROP INDEX が前に並んでる ★ → 後ろに並ばされる
T=3 INSERT C 到着 → 同様
T=N A 完了 → DROP INDEX 即実行 (一瞬で完了)
→ B, C, ... が順次解放
ポイント: DROP INDEX 自体は瞬時でも、前にいる長時間トランザクションを待つ間、後ろも詰まる。
これが「DROP INDEX は短いがリスクが大きい」と言われる理由です。
3. CREATE INDEX
3.1 通常の CREATE INDEX
CREATE INDEX idx_users_email ON users (email);
ロック: SHARE (テーブル全体)
SELECTは通す (互換)INSERT/UPDATE/DELETEは ブロック (非互換)- ★ つまり書き込みが止まる ★
所要時間: テーブルサイズに比例
- データを全件スキャンしてインデックスを構築
- 大きいテーブルだと数分〜数十分
用途:
- マイグレーション時、初回構築時
- メンテナンスウィンドウが取れるとき
3.2 CREATE INDEX CONCURRENTLY
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
ロック: SHARE UPDATE EXCLUSIVE
INSERT/UPDATE/DELETEと 互換 ★- 書き込みを止めない
所要時間: 通常 CREATE の 約 2 倍
- 2 回テーブルをスキャンする (concurrent な変更を取り込むため)
- ただしロック保持時間は短いので実害は小さい
制約:
- トランザクション内で実行不可 (自動 commit が必要)
- 失敗すると
INVALID状態のインデックスが残る → 手動 cleanup 必要 EXCLUSIVEロックを最終フェーズで一瞬取る (ms 級)- partitioned index の親には使えない (各子に対しては可能)