postgres-database-migration について
PostgreSQLの開発者や運用担当者向けのマイグレーションガイドです。ロックのリスク、段階的なバックフィル、検証、ロールバック計画を扱い、大規模な本番テーブルの変更による影響を抑えます。
主な機能
- 01DDLロックと書き換えリスクの評価
- 02ロックを抑える移行パターン
- 03データの段階的なバックフィル
- 04移行前後のデータ検証
- 05ロールバックとトランザクションの計画
活用シーン
- 01大規模テーブルへのインデックス追加
- 02カラム型の安全な変更
- 03停止時間を抑えた制約変更
- 04本番移行とロールバックの計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ timescale/ pg-aiguide --skill postgres-database-migrationインストール後、Claude Code で/postgres-database-migrationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/backfill-strategies.mdcomplete-example.mdvalidation-queries.md詳細
PostgreSQL データベースのマイグレーション
空の開発用データベースでは問題なく動作するスキーママイグレーションも、数百万行を含む本番テーブルでは失敗したり、ロックを引き起こしたり、データを破損させたりすることがあります。このガイドでは、リスクの評価、実データを使ったテスト、安全なマイグレーションの実行方法について説明します。
DDL ロックのリファレンス
スキーマを変更するたびにロックが取得されます。重要なのは、読み取りと書き込みがどの程度の時間ブロックされるかです。
高速でブロックしない操作
これらの操作はテーブルサイズに関係なく、数ミリ秒で完了します。データを書き換えるのではなく、カタログを更新するための短時間のAccessExclusiveLockのみを保持します。
| 操作 | ロックレベル | 備考 |
|---|---|---|
ADD COLUMN(NULL 許容、デフォルト値なし) | AccessExclusiveLock(短時間) | 高速です。 テーブルの書き換えは発生しません。メタデータのみを変更します。 |
ADD COLUMN ... DEFAULT x(PG 11 以降) | AccessExclusiveLock(短時間) | 高速です。 揮発性でないデフォルト値はバックフィルされず、カタログに保存されます。 |
DROP COLUMN | AccessExclusiveLock(短時間) | 高速です。 列は非表示としてマークされ、領域は時間の経過とともに VACUUM によって回収されます。 |
SET DEFAULT / DROP DEFAULT | AccessExclusiveLock(短時間) | メタデータのみを変更します。既存の行には触れません。 |
CREATE INDEX CONCURRENTLY | ShareUpdateExclusiveLock | ブロックしません。 作成中も読み取りと書き込みが可能です。通常のインデックス作成より時間がかかります。 |
DROP INDEX CONCURRENTLY | ShareUpdateExclusiveLock | ブロックしません。 インデックスを使用しているクエリの完了を待ってから削除します。テーブルレベルの排他ロックは取得しません。 |
RENAME COLUMN | AccessExclusiveLock(短時間) | メタデータのみを変更します。高速です。 |
RENAME TABLE | AccessExclusiveLock(短時間) | メタデータのみを変更します。高速です。 |
ADD CONSTRAINT ... NOT VALID | ShareUpdateExclusiveLock | 新しい行にのみ制約を追加します。既存データはスキャンしません。 |
VALIDATE CONSTRAINT | ShareUpdateExclusiveLock | 既存の行をスキャンしますが、同時に読み取りと書き込みが可能です。 |
CREATE/DROP TRIGGER | ShareRowExclusiveLock | カタログを短時間で更新します。 |
低速またはブロックを引き起こす操作
これらの操作はテーブルを書き換えるか、すべての行をスキャンします。大きなテーブルでは、数秒から数時間にわたり、すべてのアクセスがブロックされることがあります。
| 操作 | ロックレベル | 低速になる理由 |
|---|---|---|
ADD COLUMN ... DEFAULT x(揮発性の値。例:now()、gen_random_uuid()) | AccessExclusiveLock | テーブル全体を書き換えます。すべての行に計算された値が設定されます。 |
ALTER COLUMN TYPE(ほとんどの型変更) | AccessExclusiveLock | 保存済みデータを変換するため、テーブル全体を書き換えます。 |
SET NOT NULL(PG 12 未満、または既存の CHECK がない場合) | AccessExclusiveLock | NULL がないことを確認するため、テーブル全体をスキャンします。以下の安全なパターンを参照してください。 |
ADD CONSTRAINT ... CHECK/UNIQUE/FK(検証済み) | AccessExclusiveLock または ShareRowExclusiveLock | 検証のためすべての行をスキャンし、書き込みをブロックします。 |
CREATE INDEX(CONCURRENTLY なし) | ShareLock | 作成にかかる全期間、書き込みをブロックします。 |
CLUSTER | AccessExclusiveLock | インデックス順にテーブル全体を書き換えます。 |
VACUUM FULL | AccessExclusiveLock | 領域を回収するためにテーブルを書き換えます。 |
重要なポイント: AccessExclusiveLockは読み取りも書き込みもすべてブロックします。操作自体が数ミリ秒で終わる場合でも、ロックを取得する前に、実行中のすべてのトランザクションが終了するのを待つ必要があります。長時間実行されるクエリやアイドル状態のトランザクションがあると、ALTER TABLEが停止し、後続のクエリがすべてその背後に滞留することがあります。
安全なマイグレーションパターン
列を追加する
-- SAFE: nullable column, no default — instant
ALTER TABLE orders ADD COLUMN tracking_number TEXT;
-- SAFE (PG 11+): column with non-volatile default — instant
ALTER TABLE orders ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
-- UNSAFE: column with volatile default — full table rewrite
-- DON'T: ALTER TABLE orders ADD COLUMN created_at TIMESTAMPTZ DEFAULT now();
-- DO: add nullable, then backfill, then set default + NOT NULL
ALTER TABLE orders ADD COLUMN created_at TIMESTAMPTZ;
-- Backfill in batches (see Backfill section)
ALTER TABLE orders ALTER COLUMN created_at SET DEFAULT now();
ALTER TABLE orders ALTER COLUMN created_at SET NOT NULL; -- only if PG12+ or CHECK exists
列を削除する
-- SAFE: instant (column marked invisible, space reclaimed by VACUUM)
ALTER TABLE orders DROP COLUMN old_status;
アプリケーションとの連携: 列を削除する前に、アプリケーションがその列を参照しなくなっていることを確認してください。ダウンタイムなしでデプロイするには、次の2つの手順が必要です。
- 列を読み書きしないコードをデプロイします
- その後、別のマイグレーションで列を削除します
セキュリティ上の注意点: DROP COLUMN はデータを物理的に削除しません。列は pg_attribute で削除済みとしてマークされますが、VACUUM が領域を回収するまで値はディスク上に残ります。さらに、その後もスーパーユーザーなら復元できる可能性があります。列に機密データが含まれている場合は、削除後にテーブルに対して VACUUM FULL を実行するか、ダンプとリストアを使ってデータが確実に消去されるようにしてください。
列の名前を変更する
-- SAFE: instant metadata change
ALTER TABLE orders RENAME COLUMN status TO order_status;
警告: 古い列名を参照しているアプリケーションコード、ビュー、関数が動作しなくなります。ダウンタイムなしでデプロイするには、代わりに列の入れ替えパターンを使用してください。
- 新しい列を追加します
- 両方の列に書き込むコードをデプロイします
- 古い行をバックフィルします
- 新しい列から読み込むコードをデプロイします
- 古い列を削除します
列の型を変更する
ほとんどの型変更ではテーブル全体が書き換えられます。安全な代替方法は次のとおりです。
-- UNSAFE: full table rewrite, blocks everything
-- DON'T: ALTER TABLE orders ALTER COLUMN amount TYPE NUMERIC(12,2);
-- SAFE: use a new column + backfill
ALTER TABLE orders ADD COLUMN amount_new NUMERIC(12,2);
-- Backfill in batches (see Backfill section below)
UPDATE orders SET amount_new = amount WHERE id BETWEEN 1 AND 10000;
-- ... continue in batches ...
-- Swap columns
ALTER TABLE orders DROP COLUMN amount;
ALTER TABLE orders RENAME COLUMN amount_new TO amount;
例外: 書き換えが不要で、高速に実行できるキャストもあります。
| 変更前 | 変更後 | 書き換えが必要か? |
|---|---|---|
VARCHAR(n) → VARCHAR(m)(m > n の場合) | いいえ | メタデータのみ |
VARCHAR(n) → TEXT | いいえ | メタデータのみ |
NUMERIC(p,s) → NUMERIC(p2,s)(p2 > p で、スケールが同じ場合) | いいえ | メタデータのみ |
INTEGER → BIGINT | はい | 全体を書き換え |
TIMESTAMP → TIMESTAMPTZ | はい | 全体を書き換え |
NOT NULL 制約を追加する
-- PG 18+: simplified two-step pattern
ALTER TABLE orders ALTER COLUMN order_status SET NOT NULL NOT VALID;
ALTER TABLE orders VALIDATE NOT NULL ON order_status;
-- PG 12–17: fast if a valid CHECK constraint already exists
-- Step 1: add CHECK (non-blocking scan)
ALTER TABLE orders ADD CONSTRAINT orders_status_nn CHECK (order_status IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_nn;
-- Step 2: add NOT NULL (PG12+ recognizes the CHECK and skips the scan)
ALTER TABLE orders ALTER COLUMN order_status SET NOT NULL;
-- Step 3: drop the now-redundant CHECK
ALTER TABLE orders DROP CONSTRAINT orders_status_nn;
-- PG < 12: SET NOT NULL always scans the full table.
-- Ensure no NULLs exist first, then accept the brief lock.
外部キーを追加する
-- UNSAFE: validates all existing rows while holding a heavy lock
-- DON'T: ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
-- SAFE: two-step approach
-- Step 1: add without validation (blocks writes briefly, doesn't scan data)
ALTER TABLE orders ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- Step 2: validate existing rows (allows concurrent reads and writes)
ALTER TABLE orders VALIDATE CONSTRAINT fk_user;
インデックスを追加する
-- UNSAFE on large tables: blocks all writes for the entire build
-- DON'T: CREATE INDEX idx_orders_user ON orders (user_id);
-- SAFE: concurrent index creation (allows reads and writes)
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
-- IMPORTANT: if concurrent index creation fails (crashes, deadlock),
-- it leaves an INVALID index behind. Check and clean up:
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
AND indexrelname = 'idx_orders_user';
-- Check for invalid indexes
SELECT indexrelid::regclass AS index_name, indisvalid
FROM pg_index
WHERE NOT indisvalid;
-- Drop and retry if invalid
DROP INDEX CONCURRENTLY idx_orders_user;
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
一意制約を追加する
-- A UNIQUE constraint creates an index. Use CONCURRENTLY to avoid blocking:
-- Step 1: create a unique index concurrently
CREATE UNIQUE INDEX CONCURRENTLY idx_orders_tracking_uniq ON orders (tracking_number);
-- Step 2: attach it as a constraint (instant)
ALTER TABLE orders ADD CONSTRAINT orders_tracking_uniq UNIQUE USING INDEX idx_orders_tracking_uniq;
主キーを再定義する
PK の再定義(たとえば、id から複合キーへの切り替えや、int から bigint への変更)には UNIQUE 制約と NOT NULL の両方が必要で、単純に実行するとどちらも長時間ロックを引き起こす可能性があります。ダウンタイムなしで実行するには、必要な要素をそれぞれ別々に構築します。
-- Step 1: add CHECK NOT NULL constraint without validation (brief lock)
ALTER TABLE orders ADD CONSTRAINT orders_new_id_nn
CHECK (new_id IS NOT NULL) NOT VALID;
-- Step 2: validate existing rows (allows concurrent reads and writes)
ALTER TABLE orders VALIDATE CONSTRAINT orders_new_id_nn;
-- Step 3: build unique index concurrently (non-blocking)
CREATE UNIQUE INDEX CONCURRENTLY idx_orders_new_pkey
ON orders (new_id);
-- Step 4: drop the old PK
ALTER TABLE orders DROP CONSTRAINT orders_pkey;
-- Step 5: add new PK using the existing index (instant — also implicitly adds NOT NULL)
ALTER TABLE orders ADD CONSTRAINT orders_pkey
PRIMARY KEY USING INDEX idx_orders_new_pkey;
-- Step 6: drop the now-redundant CHECK constraint
ALTER TABLE orders DROP CONSTRAINT orders_new_id_nn;
この方法が有効な理由: Postgres は構築済みの一意インデックスを再利用し、既存の CHECK 制約を認識するため、手順5は高速です。その結果、インデックスの構築とテーブル全体の NOT NULL スキャンの両方が省略されます(PG12+)。
制約を削除する
-- SAFE: instant metadata change
ALTER TABLE orders DROP CONSTRAINT orders_tracking_uniq;
-- If dropping a FK that has a supporting index you no longer need:
ALTER TABLE orders DROP CONSTRAINT fk_user;
DROP INDEX idx_orders_user_id; -- only if no other queries use it
バックフィル戦略
バックフィルは必ずバッチで実行し、単一の UPDATE でまとめて実行しないでください。主キー単位のバッチ処理パターン、再開可能な進捗追跡、チューニングの指針については、backfill-strategies を参照してください。
マイグレーションの検証
各マイグレーションの前後に検証クエリを実行してください。NULL の検出、重複の検出、孤立行、キャストの失敗、実行時間の推定、スキーマの検証、データの整合性、クエリのパフォーマンスに関する一連のチェックについては、validation-queries を参照してください。
ロールバックの計画
すべてのマイグレーションについて、実行前にロールバック計画を文書化してください。
可逆な操作
| 操作 | ロールバック |
|---|---|
ADD COLUMN | DROP COLUMN |
ADD CONSTRAINT | DROP CONSTRAINT |
CREATE INDEX | DROP INDEX |
RENAME COLUMN x TO y | RENAME COLUMN y TO x |
SET DEFAULT x | SET DEFAULT old_value または DROP DEFAULT |
ADD COLUMN new + DROP COLUMN old | 直接元に戻すことはできません。古い列を再追加し、バックアップからバックフィルする必要があります |
不可逆な操作
元に戻すには、バックアップまたはデータベースのフォークから復元する必要があります。
DROP COLUMN— VACUUM が領域を回収すると、データは失われますALTER COLUMN TYPE(情報が失われるキャストを伴う場合。例:NUMERIC→INTEGER、TEXT→VARCHAR(50))- データクリーンアップ中の
DELETE/TRUNCATE DROP TABLE
データベースのフォークが非常に役立つのはこのような場合です。 マイグレーション前にフォークしておけば、元のデータベースにはマイグレーション前の状態が保たれます。マイグレーションに問題が起きても、本番データには影響がありません。フォークを削除して、やり直すだけです。
トランザクション戦略
複数の DDL ステートメントを実行する方法は 2 つあります。それぞれにトレードオフがあります。
1 つのトランザクションでまとめて実行 — すべての変更が成功するか、すべてロールバックされます。ロック時間よりも原子性を重視し、すべての操作が高速(数ミリ秒)な場合に使用してください。
BEGIN;
ALTER TABLE orders ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
ALTER TABLE orders ADD COLUMN tags TEXT[] NOT NULL DEFAULT '{}';
CREATE INDEX ON orders USING GIN (tags);
ALTER TABLE orders DROP COLUMN old_priority;
-- Verify before committing
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'orders'
ORDER BY ordinal_position;
COMMIT;
-- Or ROLLBACK; if something looks wrong
個別のトランザクションで実行 — 各 DDL が個別に実行され、コミットされます。原子性よりもロック時間を重視する場合に使用してください。1 つのトランザクションでは、すべてのロックが COMMIT まで保持されます。そのため、DDL ステートメントが 5 つある場合、最初のステートメントの AccessExclusiveLock により、5 つすべての実行が終わるまでトラフィックがブロックされます。個別のトランザクションでは、ステートメント間でロックが解放されます。
-- Each statement auto-commits
ALTER TABLE orders ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
ALTER TABLE orders ADD COLUMN tags TEXT[] NOT NULL DEFAULT '{}';
ALTER TABLE orders DROP COLUMN old_priority;
トレードオフ: 個別のトランザクションでは、後続のステートメントが失敗すると、スキーマが一部だけ移行された状態になる可能性があります。各ステップについて、個別にロールバック計画が必要です。
トランザクションを使用できないもの:
CREATE INDEX CONCURRENTLY(トランザクション内では明示的に禁止されています)DROP INDEX CONCURRENTLY- 独自のトランザクションコンテキストを必要とするステートメント
長時間実行クエリへの対処
高速な ALTER TABLE でも、長時間実行クエリの後ろで AccessExclusiveLock の取得を待っていると、停止する可能性があります。さらに、待機中の DDL は後続のクエリもすべてブロックするため、単純な SELECT でさえその後ろに滞留します。
Session 1: SELECT COUNT(*) FROM orders; -- long query, holds AccessShareLock
Session 2: ALTER TABLE orders ADD COLUMN ...; -- waits for Session 1 (needs AccessExclusiveLock)
Session 3: SELECT * FROM orders WHERE id = 123; -- BLOCKED by Session 2's lock queue entry
Session 4: INSERT INTO orders (...) VALUES (...); -- also BLOCKED
-- All sessions freeze until Session 1 finishes and Session 2 completes or times out
そのため、lock_timeout が重要です。設定しないと、1 つの遅いクエリが連鎖的にアプリケーション全体の停止を引き起こす可能性があります。
タイムアウトの設定
lock_timeout — ロック取得を諦めるまで待機する時間です。すべての本番環境の DDL ステートメントで使用してください。設定しないと、ALTER TABLE が長時間実行クエリの後ろに並び、後続のクエリを無期限にブロックする可能性があります。
statement_timeout — ロックを取得してから、ステートメントを実行できる時間です。予期せず遅い操作(たとえば、想定していなかったテーブルの再書き込みを引き起こす型変更)に対する安全策となります。トレードオフとして、操作の途中でタイムアウトすると、ステートメント全体がロールバックされます。DDL では部分的な変更が発生しないため安全ですが、長時間かかる CREATE INDEX CONCURRENTLY が完了直前に中断される可能性があります。そのため、大規模なテーブルでの並行インデックス作成など、時間がかかると分かっている操作には statement_timeout を設定せず、手動で監視してください。
タイムアウト値の選び方:
考え方は 2 つあります。
- 保守的(
lock_timeoutを 50~100ms、数百回再試行): 待機中の DDL がほかのクエリをブロックする時間を最小限に抑えます。各試行はアプリケーションのトラフィックにほとんど影響しませんが、再試行ロジックが必要です。数秒でもクエリがブロックされるのを許容できない、高トラフィックの OLTP システムに最適です。 - 実用的(
lock_timeoutを 3~5s、数回再試行): 各試行でロックを取得できる妥当な機会を設け、複雑な再試行ロジックの必要性を減らします。短い一時停止が許容されるほとんどのアプリケーションで問題ありません。
トラフィックの特性に合わせて選択してください。クエリのスループットが高いほど、lock_timeout は短くする必要があります。短時間の滞留であっても、1 秒あたりに影響を受けるクエリ数が多くなるためです。statement_timeout は、操作にかかる予想時間に十分な余裕を持たせて設定してください(たとえば、メタデータのみを変更する場合は 30s、大規模なテーブルでの VALIDATE CONSTRAINT には数分、CREATE INDEX CONCURRENTLY では無効にします)。
-- Fail fast instead of blocking all queries behind you
SET lock_timeout = '5s';
SET statement_timeout = '30s';
ALTER TABLE orders ADD COLUMN tracking_number TEXT;
-- If it fails with "canceling statement due to lock timeout":
-- 1. Find what's blocking
SELECT pid, state, query, now() - query_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
-- 2. Wait for the blocker to finish, or cancel it if appropriate
-- SELECT pg_cancel_backend(<pid>);
-- 3. Retry the ALTER TABLE
SET lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN tracking_number TEXT;
-- Reset timeouts when done
RESET lock_timeout;
RESET statement_timeout;
タイムアウト付き再試行パターン
自動マイグレーションランナーでは、短いロックタイムアウトを設定し、再試行ループで DDL を実行してください。
DO $$
DECLARE
max_attempts INTEGER := 5;
attempt INTEGER := 1;
success BOOLEAN := FALSE;
BEGIN
WHILE attempt <= max_attempts AND NOT success LOOP
BEGIN
SET lock_timeout = '3s';
-- Replace with your DDL statement
ALTER TABLE orders ADD COLUMN tracking_number TEXT;
success := TRUE;
RAISE NOTICE 'DDL succeeded on attempt %', attempt;
EXCEPTION
WHEN lock_not_available THEN
RAISE NOTICE 'Attempt % failed (lock not available), retrying...', attempt;
PERFORM pg_sleep(2 * attempt); -- linear backoff
attempt := attempt + 1;
END;
END LOOP;
IF NOT success THEN
RAISE EXCEPTION 'DDL failed after % attempts', max_attempts;
END IF;
END $$;
これにより、マイグレーションが後続にブロックされたクエリを滞留させるのを防げます。各試行はすぐに成功するか、諦めて通常のトラフィックを通します。
代替手段:NOWAIT — トラフィックが最も多いシステムでは、DDL の実行前にロックが取得可能かどうかを確認するため、LOCK TABLE ... NOWAIT を使用してください。lock_timeout とは異なり、NOWAIT はロック待ちキューに入ることなく即座に失敗するため、クエリが連鎖的にブロックされるリスクがありません。トレードオフとして、再試行回数が増えます。
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;
-- If we get here, we have the lock — run DDL
ALTER TABLE orders ADD COLUMN tracking_number TEXT;
COMMIT;
-- If LOCK fails with "could not obtain lock", retry after a short sleep
フォークベースのマイグレーションテスト
マイグレーションをテストする最も安全な方法は、実際のデータベースのコピーに対して実行することです。スキーマもデータもエッジケースも、すべて同じ状態でテストできます。Neon などのプロバイダーは、高速なデータベースのフォークに対応しています。データベースのフォークができない場合は、データベースを手動でダンプして復元する必要があり、大規模なデータセットでは時間がかかることがあります。
フォークを利用する場合
- データベースをフォークする — プロバイダーのフォーク機能を使って完全なコピーを作成します(数秒で完了します)
- フォーク上の現在のスキーマを確認し、本番環境と一致していることを確かめます
- フォーク上でマイグレーションを実行します
- 検証する — チェックを実行します(上記のマイグレーション前/マイグレーション後の検証のセクションを参照)
- 成功した場合: 同じマイグレーションを本番環境に適用します
- 失敗した場合: フォークを削除します。本番データベースには影響ありません
これにより、空のテストデータベースでは決して発生しない問題を検出できます。
- 新しい制約に違反するデータ
- 実際の値に対して失敗する型キャスト
- 100 行では高速でも、1,000 万行ではテーブルを数分間ロックするマイグレーション
- メモリまたはディスク容量が不足するインデックス作成
制限事項: フォークベースのテストでは、マイグレーションを独立して実行するため、同時に発生するデータベーストラフィックに起因する問題(負荷時のロック競合、同時書き込みによるデッドロック、大量の WAL 生成によるレプリケーション遅延など)は検出できません。ほとんどのアプリケーションでは、フォークベースのテストで十分です。稼働率が非常に高いアプリケーションでは、PgDog のミラーリング機能を使って、本番トラフィックをフォークに再生してください。これにより、現実的なタイミングでクエリがバイト単位で再現されます。また、DDL のみ、または DML のみに絞り、公開する割合を制御して段階的に拡大できます。
フォークを利用しない場合
バックアップまたはダンプからテスト用データベースを作成します。
# Dump your production database
pg_dump -Fc my_app_db > backup.dump
# Restore into a test database
createdb migration_test
pg_restore -d migration_test backup.dump
# Or clone from a live database (requires downtime on source during copy)
createdb migration_test -T my_app_db
マイグレーションの完全な例
計画、フォーク、実行、検証、適用、クリーンアップまでを網羅したエンドツーエンドの手順については、完全な例を参照してください。
高度な考慮事項
PL/pgSQL の再試行ループ内のサブトランザクション: 再試行とタイムアウトのパターンで使われる BEGIN/EXCEPTION WHEN/END ブロックは、暗黙的なサブトランザクションを作成します。書き込みスループットが高いと、レプリカで SubtransSLRU の競合が発生する場合があります。特に、再試行ループを長時間実行されるトランザクションとして、多数の試行を行うと発生しやすくなります。再試行中にレプリカの遅延が見られる場合は、PL/pgSQL の例外処理を使うのではなく、再試行ロジックをアプリケーション層に移し、試行ごとにトランザクションを分けてください。
Autovacuum によって VALIDATE CONSTRAINT がブロックされることがあります: VALIDATE CONSTRAINT は ShareUpdateExclusiveLock を取得します。このロックは、トランザクション ID のラップアラウンドを防止するモードで実行中の autovacuum と競合します。VALIDATE が予期せず停止する場合は、同じテーブルに対する autovacuum プロセスがないか、pg_stat_activity を確認してください。ラップアラウンド防止のための autovacuum が完了するまで待つ必要がある場合があります。これをキャンセルしないでください。テーブルが XID のラップアラウンド上限に近づいている場合、データ損失につながるおそれがあります。
よくある落とし穴
- 空のテーブルでマイグレーションをテストする — 空のテーブルでは1msで完了するマイグレーションでも、1,000万行のテーブルを数分間ロックすることがあります。必ず現実的なデータ量でテストしてください。
- インデックス作成時に
CONCURRENTLYを付け忘れる —CREATE INDEX(CONCURRENTLYなし)はすべての書き込みをブロックします。トラフィックがあるテーブルでは、これによりダウンタイムが発生します。 - 2段階のパターンを使わずに NOT NULL を追加する — PG < 12 の大規模テーブルでは、
SET NOT NULLがAccessExclusiveLockを保持したまま全行をスキャンします。CHECK 制約のパターンを使用してください。 - ロックタイムアウトを設定しない — 短時間で終わる ALTER TABLE でも、長時間実行中のクエリの後ろでブロックされ、その後のクエリがすべてその後ろに積み重なります。本番環境の DDL では、必ず
SET lock_timeoutを設定してください。 - バックフィルを一度の大きなトランザクションで行う — 1,000万行に対する単一の
UPDATE orders SET x = yは、大量の WAL を生成し、テーブルを肥大化させ、処理中ずっとロックを保持します。必ずバッチ処理してください。 - 無効なインデックスを残す —
CREATE INDEX CONCURRENTLYが失敗すると、領域を消費して書き込みを遅くする、不可視の無効なインデックスが残ります。並行インデックス操作のたびにpg_index.indisvalidを確認してください。 - アプリケーションコードを更新する前に列を削除する — システムの稼働中は、古いコードがその列を参照し続けます。先にコード変更をデプロイし、その後のマイグレーションで列を削除してください。
- レプリケーションラグを確認しない — 大規模なバックフィルは大量の WAL を生成します。読み取りレプリカがある場合は、マイグレーションの実行中と実行後に
pg_stat_replicationを監視してください。 - ALTER COLUMN TYPE は安全だと思い込む — ほとんどの型変更では、テーブル全体が書き換えられます。大規模テーブルでは、新しい列を追加してバックフィルし、入れ替えるパターンを使用してください。