2025年5月26日·1分で読めます

PostgreSQLでゼロダウンタイムのインデックス変更:安全なプレイブック

CONCURRENTLY、簡単なロックチェック、明確なロールバック手順を使った PostgreSQL のゼロダウンタイムなインデックス変更で本番トラフィックを維持します。

PostgreSQLでゼロダウンタイムのインデックス変更:安全なプレイブック

インデックス変更がダウンタイムを引き起こす理由(と回避方法)

インデックス作業は無害に聞こえます。「ただ」補助構造を追加するだけです。しかし PostgreSQL では、インデックスの作成・削除・交換は他のセッションをブロックするロックを伴うことがあります。テーブルが忙しいと待ちが積み重なり、アプリが壊れたように見え始めます。

ダウンタイムはめったにきれいな停止通知のようには現れません。ページが固まる、バックグラウンドジョブが遅れる、データベースのためにリクエストのキューが増える、といった形で表れます。誰かが「検索」を押してタイムアウトになる一方で、サポートツールや管理画面が遅く感じるのは、単純なクエリが必要なロックを取得できないからです。

「夜にやればいい」は二つのよくある理由で失敗します。多くのシステムは真に静かになることがなく(グローバルユーザー、バッチ、ETL、バックアップなど)、またインデックス操作は大量のデータを読むため想定より長くかかることがあります。ウィンドウが途中で閉じると、待つか作業を中断するかの選択を迫られます。

ゼロダウンタイムのインデックス変更は魔法ではありません。最小限にブロックする操作を選び、ガードレール(タイムアウトやディスクチェック)を設定し、実行中にデータベースを監視するという習慣に尽きます。

このプレイブックは実践的な本番運用の習慣に焦点を当てます:

  • 読み書きを止められない場合は並行ビルド(concurrent)を優先する。
  • ロックとビルド進行を監視して早く対処する。
  • 変更が問題を起こすか長引く場合に備えロールバック経路を用意する。

ここで扱わないもの:深いインデックス設計理論、広範なクエリチューニング、または大量データを書き換えるスキーマリファクタリング。

インデックス作業に関する単純なロックモデル

PostgreSQL は多くのセッションが同じテーブルを扱うときにデータの整合性を保つためロックを使います。ロックは今誰がオブジェクトを読める/書けるか、誰が待たなければならないかを決めるルールです。

普段は PostgreSQL が軽量なモードを使うためロックに気づかないことが多いです。DDL は違います。インデックスを作成・削除するとき、PostgreSQL はカタログとデータの一貫性を保つためにテーブルに対して十分な制御が必要です。必要な制御が強ければ強いほど、他のセッションが待たされる可能性が高くなります。

インデックスの使用と作成の違い

インデックスを使うことは通常ロック観点で廉価です。SELECT、UPDATE、DELETE は他のセッションと同時にインデックスを読み書きできます。

インデックスを作る作業は異なります。PostgreSQL はテーブルをスキャンし、キーをソートやハッシュし、新しい構造をディスクに書き込みます。その作業は時間を要し、それが "小さなロック" を本番で "大きな問題" に変えます。

CONCURRENTLY が変えること(と変えないこと)

通常の CREATE INDEX は書き込みをブロックする強いロックを取ります。CREATE INDEX CONCURRENTLY はインデックス作成中も通常の読み書きを継続できるよう設計されています。

しかし「concurrent」が「ロックフリー」を意味するわけではありません。開始と終了に短いロック窓があり、互換性のないロックがあるとビルドは失敗または待機することがあります。

重要な結果は次の通りです:

  • 非並行ビルド(non-concurrent)はテーブルへのINSERT/UPDATE/DELETEをブロックする可能性があります。
  • 並行ビルドは通常読み書きを許しますが、長時間のトランザクションで遅くなったり止まったりします。
  • 終了処理は短いロックを必要とするため、非常に忙しいシステムでは短い待ちが発生します。

適切なアプローチを選ぶ:concurrentか通常か

インデックスを変更する際の主な選択肢は二つ:通常の方法でインデックスを作る(速いがブロッキングする)、または CONCURRENTLY で作る(通常アプリのトラフィックを止めないが遅く、長時間トランザクションに敏感)。

CONCURRENTLY を選ぶべきとき

テーブルが実際のトラフィックに使われていて書き込みを止められない場合は CREATE INDEX CONCURRENTLY を使います。通常次のような場合に安全な選択です:

  • テーブルが大きく、通常のビルドが数分から数時間かかる可能性がある。
  • テーブルに継続的な書き込みがある(読み取りだけでない)。
  • 真のメンテナンスウィンドウを確保できない。
  • まず新しいインデックスを作って検証し、その後古いインデックスを削除したい。

通常のインデックスビルドが許容できるとき

テーブルが小さい、トラフィックが低い、制御されたウィンドウがある場合は通常の CREATE INDEX で問題ありません。通常は速く、実行も単純です。

ステージングで一貫して速く終わることが確認でき、書き込みを一時的に止められるなら通常の方法を検討してください。

一意性が必要なら早めに判断しましょう。CREATE UNIQUE INDEX CONCURRENTLY は動作しますが、重複があると失敗します。本番では重複の検出と修正が実作業になることが多いです。

本番に触る前の事前チェック

多くの問題はコマンドを開始する前に起きます。いくつかのチェックで予期せぬブロッキングと、予想より長く/多くのディスクを使うビルドを避けられます。

  1. トランザクション中でないことを確認する。CREATE INDEX CONCURRENTLYBEGIN 後では失敗します。GUI ツールが静かにステートメントをトランザクションに包むことがあるので注意してください。確実でなければ新しいセッションを開き、そのセッションでインデックスコマンドだけ実行します。

  2. 時間とディスクの見積もりを立てる。並行ビルドは通常通常のビルドより長くかかり、実行中に追加の作業領域を必要とします。新しいインデックス分と一時的なオーバーヘッドを見込み、十分な空きディスクがあることを確認してください。

  3. 目的に合ったタイムアウトを設定する。ロックが取れないなら速やかに失敗して欲しいが、攻撃的すぎる statement timeout でビルドが途中で死ぬのは避けたい、というバランスを取ります。

  4. ベースラインを取得する。変更が有効だった証拠と回帰の検出を素早く行えるように、事前に遅いクエリの計測、代表的な EXPLAIN (ANALYZE, BUFFERS)、CPU/IO/接続数/空きディスクの簡易的なスナップショットを記録してください。

多くのチームが出発点として使う安全なセッション設定(組織に合わせて調整してください):

-- Run in the same session that will build the index
SET lock_timeout = '2s';
SET statement_timeout = '0';

ステップバイステップ:CONCURRENTLYでインデックスを作る

ドラッグなしでAPIを追加
エンドポイントとビジネスロジックを視覚的に設計し、ボイラープレートなしでデプロイします。
APIを作る

アプリケーションのトラフィックを維持したい場合、そしてより長いビルド時間を許容できる場合は CREATE INDEX CONCURRENTLY を使います。

まず何を作るかを正確に決めます:

  • カラムの順序を明確にする(重要です)。
  • 部分インデックス(partial index)で十分か検討する。多くのクエリが status = 'active' のように絞っているなら、部分インデックスは小さく、速く、保守コストも低くなります。

安全な実行例:目的とインデックス名を書き出し、トランザクションブロック外でビルドを実行し、完了するまで監視し、その後プランナーが使えることを検証してから古いものを削除します。

-- Example: speed up searches by email for active users
CREATE INDEX CONCURRENTLY idx_users_active_email
ON public.users (email)
WHERE status = 'active';

-- Validate it exists
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'users';

-- Check the plan can use it
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM public.users
WHERE status = 'active' AND email = '[email protected]';

進捗メモ(監査に便利):開始時刻、終了時刻、観測した待ちを記録します。実行中は別セッションから pg_stat_progress_create_index をクエリして進捗を確認できます。

検証は「インデックスが存在する」だけではありません。プランナーがそれを選べるか確認し、デプロイ後の実際のクエリ時間を観察します。新しいインデックスが使われないなら、古いものを急いで削除しないでください。まずクエリかインデックス定義を直しましょう。

ステップバイステップ:ブロッキングなしでインデックスを置換/削除する

最も安全なパターンは先に新しいものを追加し、トラフィックが新しいインデックスから利益を得ていることを確認してから古いものを削除することです。こうすれば動作するフォールバックが残ります。

古いインデックスを新しいものと入れ替える(安全な順序)

  1. CREATE INDEX CONCURRENTLY で新しいインデックスを作成。

  2. それが使われていることを検証。遅いクエリで EXPLAIN を確認し、インデックス使用状況を観察する。

  3. その後で古いインデックスを DROP INDEX CONCURRENTLY で削除する。リスクが高ければビジネスサイクル一周分は両方残しておく。

インデックスの削除:CONCURRENTLY が使える場合と使えない場合

通常自分で作成したインデックスなら DROP INDEX CONCURRENTLY が正しい選択です。ただし注意点が二つあります:トランザクションブロック内では実行できないこと、開始と終了で短いロックが必要なため長時間のトランザクションにより遅延することです。

インデックスが PRIMARY KEYUNIQUE 制約によって存在する場合は通常直接削除できません。ALTER TABLE で制約を変更する必要があり、これにはより強いロックがかかるため別途計画したメンテナンスになります。

可読性のためのインデックス名変更

ALTER INDEX ... RENAME TO ... は通常速いですが、ツールやマイグレーションが名前を参照している場合は避けてください。最初から分かりやすい名前を付ける習慣を持つと安全です。

古いインデックスがまだ必要な場合

場合によっては二つの異なるクエリパターンがそれぞれ別のインデックスを必要とします。重要なクエリがまだ古いインデックスに依存しているなら、それを残してください。無理に削除するより、新しいインデックスの列順や部分条件を調整することを検討します。

ビルド中のロックと進捗を監視する

運用を安全に自動化
壊れやすいスクリプトを、チームで保守できる明確なアプリとプロセス自動化に置き換えます。
無料で始める

CREATE INDEX CONCURRENTLY を使っていても、実行中に何が起きているかを監視すべきです。驚きのインシデントの多くは、見落としたブロッキングセッションか、長時間のトランザクションが原因でビルドが止まることにあります。

ブロッキングセッションの見つけ方(誰が誰をブロックしているか)

まずロックを待っているセッションを見つけます:

SELECT
  a.pid,
  a.usename,
  a.application_name,
  a.state,
  a.wait_event_type,
  a.wait_event,
  now() - a.xact_start AS xact_age,
  left(a.query, 120) AS query
FROM pg_stat_activity a
WHERE a.wait_event_type = 'Lock'
ORDER BY xact_age DESC;

正確なブロッカーが必要なら、blocked_pidblocking_pid に追跡します:

SELECT
  blocked.pid  AS blocked_pid,
  blocking.pid AS blocking_pid,
  now() - blocked.xact_start AS blocked_xact_age,
  left(blocked.query, 80)  AS blocked_query,
  left(blocking.query, 80) AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY (pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

ビルド進捗と「詰まり」サインの監視

PostgreSQL はインデックスビルドの進捗を公開します。長時間動きがないなら長時間トランザクション(古いスナップショットを保持するアイドルセッション)を探してください。

SELECT
  pid,
  phase,
  lockers_total,
  lockers_done,
  blocks_total,
  blocks_done,
  tuples_total,
  tuples_done
FROM pg_stat_progress_create_index;

またシステムの圧力(ディスクIO、レプリケーション遅延、増加するクエリ時間)にも注意してください。並行ビルドは稼働率に優しいですが大量の読み取りを行います。

本番で有効な単純なルール:

  • 進捗が動いていてユーザー影響が小さいなら待つ。
  • 長時間のトランザクションのためにビルドが詰まっている場合、安全に終わらせられないならキャンセルして再スケジュールする。
  • ピーク時間帯にIOが顧客向けクエリを圧迫しているなら中断する。
  • 最終手段としてのみ終了し、その前にセッションの状況を確認する。

チームとのコミュニケーションは短く保ちます:開始時刻、現在のフェーズ、何がブロックしているか(あれば)、次にいつ確認するか。

ロールバック計画:安全に元に戻す方法

本物のバックエンドを生成
ビジュアルデザイナーでデータをモデリングし、プロダクション対応のGoバックエンドを生成します。
バックエンドを作成

インデックス変更は、開始前に出口(ロールバック)を計画しておくことで低リスクのままにできます。最も安全なロールバックは派手な元に戻しではなく、単に新しい作業を止めて古いインデックスをそのまま残すことです。

インデックス作業が失敗する一般的な理由

本番での失敗の多くは予測可能です:ビルドがタイムアウトする、インシデント中に誰かがキャンセルする、サーバーのディスクが不足する、またはビルドが通常トラフィックと競合してユーザー向けのレイテンシが上がる、などです。

CREATE INDEX CONCURRENTLY はキャンセルしてもアプリ側は通常安全です(クエリは動き続けます)。代わりにクリーンアップが必要になることがあります:キャンセルや失敗で無効なインデックスが残ることがあります。

安全なキャンセルとクリーンアップのルール

並行ビルドをキャンセルしても通常のトランザクションのようにロールバックされません。PostgreSQL は存在するがプランナーで使えないインデックスを残すことがあります。

-- Cancel the session building the index (use the PID you identified)
SELECT pg_cancel_backend(\u003cpid\u003e);

-- If the index exists but is invalid, remove it without blocking writes
DROP INDEX CONCURRENTLY IF EXISTS your_index_name;

削除する前に状況を確認してください:

SELECT
  c.relname AS index_name,
  i.indisvalid,
  i.indisready
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname = 'your_index_name';

indisvalid = false ならプランナーで使われず、削除しても安全です。

既存のインデックスを置換する際の実用的なロールバックチェックリスト:

  • 新しいインデックスが完全にビルドされて有効になるまで古いインデックスを保持する。
  • 新しいビルドが失敗またはキャンセルされたら、無効な新インデックスを CONCURRENTLY で削除する。
  • 既に古いインデックスを削除してしまっていたら、以前の状態を復元するために CREATE INDEX CONCURRENTLY で再作成する。
  • ディスク不足が原因ならまず空き容量を確保してから再試行する。
  • タイムアウトが原因なら騒がしくないウィンドウにスケジュールし直す。

例:管理向け検索用に新しいインデックスを作り、20分ほど実行したところでディスクアラートが出た。ビルドをキャンセルし、無効なインデックスを並行で削除し、古いインデックスを残しておけば、空きを確保してから再度試してもユーザーに見える停止は発生しません。

驚きの停止を招くよくあるミス

インデックス周りのほとんどの停止は PostgreSQL が「遅い」から起きるのではありません。小さな詳細が安全な変更をブロッキングするものに変えてしまうことが原因です。

1) 並行ビルドをトランザクション内で実行する

CREATE INDEX CONCURRENTLY はトランザクションブロック内で実行できません。多くのマイグレーションツールはデフォルトで変更を一つの大きなトランザクションに包みます。結果は最良でもハードエラー、最悪はリトライの多い混乱したデプロイになります。

マイグレーションを実行する前に、ツールが外側のトランザクションなしでステートメントを実行できるか、あるいはその変更を非トランザクショナルな特別ステップに分割できるか確認してください。

2) ピークトラフィック中に開始する

並行ビルドはブロッキングを減らしますが、追加の読み取り・書き込みを増やし autovacuum にも負荷を与えます。デプロイウィンドウでトラフィックがスパイクしていると、遅延が発生して停止のように感じられることがよくあります。

静かな期間を選び、通常のメンテナンスと同様に扱ってください。

3) 長時間実行されるトランザクションを無視する

単一の長時間トランザクションが並行ビルドのクリーンアップフェーズを妨げることがあります。インデックスは進んでいるように見えて終盤で古いスナップショットを待って止まることがあります。

習慣として、開始前に長時間トランザクションをチェックし、進捗が停滞したら再確認してください。

4) 間違ったものを削除する(または制約を壊す)

チームが記憶だけでインデックス名を指定して削除したり、一意性制約を支えるインデックスを誤って消してしまうことがあります。間違ったオブジェクトを削ると制約が失われたり、クエリ性能が即座に悪化したりします。

素早い安全チェックリスト:カタログでインデックス名を確認する、そのインデックスが制約を支えているか確認する、スキーマとテーブルを二重チェックする。「新しく作る」と「古いのを削る」は分けて行い、開始前にロールバックコマンドを用意しておく。

現実的な例:管理画面の検索を速くする

検索を瞬時に感じさせる
Vue3でWebアプリを公開し、Postgresのインデックスやクエリとスムーズに連携させましょう。
Webアプリを作る

管理画面での検索はステージングでは瞬時でも本番では遅くなることがよくあります。例えば内部の管理パネルの背後に数千万行の tickets テーブルがあり、エージェントが「ある顧客の未解決チケットを最新順で」よく検索するとします。

クエリは次のようになっています:

SELECT id, customer_id, subject, created_at
FROM tickets
WHERE customer_id = $1
  AND status = 'open'
ORDER BY created_at DESC
LIMIT 50;

完全な (customer_id, status, created_at) のインデックスは役立ちますが、すべてのチケット更新で書き込みオーバーヘッドが増えます。大部分の行が open でないなら、部分インデックスが簡単で効果的な勝ちです:

CREATE INDEX CONCURRENTLY tickets_open_by_customer_created_idx
ON tickets (customer_id, created_at DESC)
WHERE status = 'open';

本番での安全なタイムライン:

  • 事前確認:クエリ形状が安定していること、新しいインデックス用に十分な空きディスクがあることを確認。
  • ビルド:別セッションで CREATE INDEX CONCURRENTLY を実行し、明確なタイムアウト設定を使う。
  • 検証:ANALYZE tickets; を実行し、プランナーが新しいインデックスを使うことを確認。
  • クリーンアップ:自信がついてから、冗長になった古いインデックスを DROP INDEX CONCURRENTLY で削除。

成功の目安:

  • 管理検索が数秒から数十ミリ秒に短縮される。
  • ビルド中も通常の読み書きは継続する。
  • ビルド中に CPU とディスク IO は上がるが安全圏内に留まる。
  • 比較できるビフォー/アフターの数値(クエリ時間、スキャン行数、ロック履歴)がある。

クイックチェックリストと次のステップ

インデックス作業は小さな本番リリースのように扱うと安全です:準備し、実行中は監視し、結果を検証してからクリーンアップします。

開始前:

  • 想定外のロックで永遠にハングしないようタイムアウトを設定する。
  • 新しいインデックスビルド用に十分な空きディスクがあるか確認する。
  • ビルドを遅くする可能性のある長時間トランザクションを探す。
  • 低トラフィックのウィンドウを選び、「完了」の定義を決める。
  • 今すぐロールバック計画を書き出す。

実行中:

  • ブロッキングとロック待ちを監視する。
  • pg_stat_progress_create_index で進捗を追う。
  • アプリの症状(エラー率、タイムアウト、当該テーブルに関連する遅いエンドポイント)を注視する。
  • ロック待ちが増えたりユーザー向けタイムアウトが上がったらキャンセルする準備をする。
  • 何が起きたかをログに残す:開始時刻、終了時刻、アラートなど。

終了後:インデックスが有効であることを確認し、主要クエリでプランと実行時間が改善しているかを確かめてから、非ブロッキングな方法で古いインデックスを削除します。

これを複数回行うなら、繰り返し実行できるデリバリ手順に落とし込みましょう:小さなランブック、プロダクションに近いデータでのステージングリハーサル、そしてビルドを監視する明確なオーナーを決めること。

もし AppMaster (appmaster.io) で内部ツールや管理画面を作っているなら、データベース変更(インデックスビルドなど)もバックエンドの更新と同じリリースチェックリストに入れて、計測・監視・迅速に実行できるロールバックを備えると便利です。

よくある質問

なぜインデックスの追加や変更でダウンタイムが起きるのですか?

ダウンタイムは通常、完全な停止ではなくロック待ちとして現れます。通常の CREATE INDEX はビルド中に書き込みをブロックするため、INSERT/UPDATE/DELETE を必要とするリクエストが待たされタイムアウトし、ページが固まったりキューが溜まったりします。

通常の CREATE INDEX の代わりに CREATE INDEX CONCURRENTLY をいつ使うべきですか?

CREATE INDEX CONCURRENTLY はテーブルに実際のトラフィックがあり書き込みを止められない場合に使ってください。大きくて忙しいテーブルでは、安全なデフォルトです。ただし実行は遅く、長時間実行中のトランザクションにより遅延することがあります。

CONCURRENTLY は「まったくロックがない」という意味ですか?

いいえ。ブロッキングを減らしますが、完全にロックがないわけではありません。開始と終了に短いロック窓があり、互換性のないロックを他セッションが持っている場合や、長時間のトランザクションがあるとビルドは待たされます。

「夜にやればいい」という考えがなぜよく失敗するのですか?

本番が静かでないことが多く、インデックスビルドはテーブルサイズやCPU/ディスクの影響で予想より長くかかるためです。ウィンドウを超えて走ると、営業時間内にリスクを延ばすか途中で中止するかの選択を迫られます。

本番で concurrent なインデックスビルドを実行する前に何を確認すべきですか?

まず CREATE INDEX CONCURRENTLY をトランザクション内で実行していないことを確認してください(失敗します)。次に新しいインデックスと一時領域のための十分な空きディスクがあるか確認し、lock_timeout を短くして必要なロックが取れなければ速やかに失敗するようにします。

安全なインデックス変更にはどんなタイムアウトを設定すべきですか?

多くのチームが出発点として SET lock_timeout = '2s';SET statement_timeout = '0'; を同じセッションで設定します。これによりロックで永遠に待たされるのを避けつつ、過度に厳しい statement timeout でビルドが途中で殺されることも防げます。

CONCURRENT なインデックスビルドが詰まっているかどうかはどう見分け、最初に何を確認しますか?

まず pg_stat_progress_create_index を見てフェーズやブロック/タプルの進捗を確認します。進捗が止まっている場合は pg_stat_activity でロック待ちを調べ、古いスナップショットを保持しているアイドルな長時間トランザクションがないか探します。

既存のインデックスをトラフィックを止めずに置き換える最も安全な方法は?

新しいインデックスをまず作成し、プランナーがそれを使っていること(および実際のクエリ時間が改善していること)を確認してから、古いインデックスを CONCURRENTLY で削除します。こうすることで新しいインデックスが使えない場合でもフォールバックが残ります。

常にインデックスを CONCURRENTLY で削除できますか?

DROP INDEX CONCURRENTLY は通常のインデックスには安全ですが、開始と終了に短いロックが必要でトランザクション内で実行できません。インデックスが PRIMARY KEY や UNIQUE 制約を支えている場合は直接削除できないことが多く、ALTER TABLE で制約を扱う必要があり、強いロックや計画が必要です。

CONCURRENT なインデックスビルドが失敗またはキャンセルされた場合、安全にロールバックするには?

ビルド中のセッションをキャンセルし、無効なインデックスが残っていないか確認します。indisvalid が false ならそれはプランナーで使われないため DROP INDEX CONCURRENTLY で削除し、古いインデックスを既に削除してしまっていたら CREATE INDEX CONCURRENTLY で再作成して元に戻します。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める