PostgreSQL マイグレーション は何をしますか?
最重要ポイント. 各マイグレーションは up/down のペアであり、down は up を完全にロールバックします。マイグレーションは自身のトランザクション内で実行します。例外は1つだけ: すでにデータがあり書き込みが行われているテーブルへのインデックス作成 — CREATE INDEX CONCURRENTLY をトランザクションなしの別マイグレーションで、ファイルに1コマンドだけ記述します。
アサーションの検証は CHECKS.md を参照。
ファイル形式
migrations/
├── 20250524120000_create_users.up.sql
├── 20250524120000_create_users.down.sql
├── 20250525090000_add_users_phone.up.sql
└── 20250525090000_add_users_phone.down.sql
- プレフィックスは UTC 時刻
YYYYMMDDHHmmss、名前は snake_case で簡潔な説明。 *.up.sqlが適用、*.down.sqlがロールバック。両方とも必須。- 適用済みのマイグレーションは編集せず、新しいものを作成すること。
型と制約
| 代わりに | こちらを使う | 理由 |
|---|---|---|
bigserial | bigint GENERATED ALWAYS AS IDENTITY | SQL 標準; インポート時に ID を挿入する必要がある場合は BY DEFAULT を使用 |
varchar(n) | text NOT NULL CHECK (length(x) <= n) | 制限はデータルールであり、変更が容易 |
timestamp | timestamptz | 「壁時計」ではなく、時点を保存 |
integer フラグ | boolean | |
json | jsonb | バイナリ形式、インデックス、演算子 |
| 拡張機能の uuid | uuid DEFAULT gen_random_uuid() | PostgreSQL 13 以降に組み込み |
テキストカラムの長さは、明らかに長いテキスト(説明、コメント)を除き、常に CHECK で制限すること。最大長が不明な場合は、ユーザーに確認すること。
| データ | 制約 |
|---|---|
CHECK (length(email) <= 320) | |
| 名前 | CHECK (length(name) <= 255) |
| 電話番号 | CHECK (length(phone) <= 50) |
| slug、コード | CHECK (length(code) <= 100) |
| URL | CHECK (length(url) <= 2048) |
| 説明 | 制限なし、または CHECK (length(bio) <= 10000) |
テーブルテンプレート:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL CHECK (length(name) <= 255),
email text NOT NULL CHECK (length(email) <= 320),
is_active boolean NOT NULL DEFAULT true,
metadata jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX ix_users_email ON users (email);
ロールバック
| up | down |
|---|---|
CREATE TABLE t | DROP TABLE IF EXISTS t |
ALTER TABLE t ADD COLUMN c | ALTER TABLE t DROP COLUMN IF EXISTS c |
CREATE INDEX ix | DROP INDEX IF EXISTS ix |
CREATE INDEX CONCURRENTLY ix | DROP INDEX CONCURRENTLY IF EXISTS ix — こちらもトランザクションなし |
CREATE TYPE t AS ENUM (...) | DROP TYPE IF EXISTS t |
ALTER TYPE t ADD VALUE 'v' | PostgreSQL に DROP VALUE はない — 型を再作成するか、空の down にする |
ALTER TABLE t ALTER COLUMN c SET NOT NULL | ALTER TABLE t ALTER COLUMN c DROP NOT NULL |
ALTER TABLE t ADD CONSTRAINT c | ALTER TABLE t DROP CONSTRAINT IF EXISTS c |
down に IF EXISTS を付けることで、ロールバックがべき等になります。
典型的な変更
デフォルト値付きカラム. 定数 DEFAULT の場合、テーブルは再書き込みされず(PostgreSQL 11+)、コマンドは瞬時に完了します。volatile なデフォルト(gen_random_uuid()、clock_timestamp())の場合、テーブル全体が排他ロックのもとで再書き込みされます。大規模なライブテーブルでは、デフォルトなしでカラムを追加し、バッチで埋めてください。
ALTER TABLE users ADD COLUMN role text NOT NULL DEFAULT 'user' CHECK (length(role) <= 50);
外部キー — 参照側のカラムにはすぐにインデックスを作成してください。PostgreSQL は自動で作成せず、インデックスがないと親の削除や JOIN がシーケンシャルスキャンになります。
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users (id),
total numeric NOT NULL
);
CREATE INDEX ix_orders_user_id ON orders (user_id);
Enum への値追加. ALTER TYPE ... ADD VALUE はトランザクション内で動作しますが、新しい値はコミットまで使用できません(55P04 unsafe use of new value)。マイグレーターがトランザクション内でマイグレーションを実行する場合、値の追加とその使用(UPDATE、DEFAULT、CHECK)は 異なる マイグレーションで行ってください。
ALTER TYPE payment_method ADD VALUE 'bank_transfer';
down: DROP VALUE はありません。値に問題がなければ、コメント付きで down を空にしておきます。問題があれば、型を再作成(新しい型、ALTER COLUMN ... TYPE ... USING、古い型の削除)します。
ライブテーブルへのインデックス
すでにデータがあり書き込みが行われているテーブルでは、CREATE INDEX CONCURRENTLY (CIC) を使用します。通常の CREATE INDEX はビルド中ずっと書き込みをブロックします。新しいテーブルと同時に同じマイグレーションでインデックスを作成する場合は、通常の CREATE INDEX を使用します。データベースのサイズは関係ありません: 長時間実行中のトランザクションは、どのサイズでも新しいインデックスを壊します(後述)。
up:
CREATE INDEX CONCURRENTLY ix_orders_customer_id ON orders (customer_id);
down:
DROP INDEX CONCURRENTLY IF EXISTS ix_orders_customer_id;
- トランザクションなしの個別マイグレーション、ファイルに1コマンドのみ。 トランザクションブロック内では CIC は禁止されています(「トランザクションブロック内では実行できません」)。1つのクエリとして送信された複数のコマンドは、PostgreSQL によって暗黙のトランザクションとして実行され、CIC は同じエラーで失敗します。
- マイグレーターがトランザクションなしのマイグレーションをサポートしていない場合、CIC を通常のマイグレーションに入れないでください。 ユーザーにそのモードが必要であることを伝えるか(「マイグレーターに求めること」セクション参照)、CIC を別途実行し、マイグレーションを手動で記録してください。
- CIC は、古いスナップショットを持つすべてのトランザクションの終了を待ちます。 他のテーブルを読んでいるだけのトランザクションでも同様です。マイグレーションに時間がかかる可能性があります。
- 適用後、インデックスを確認してください。 CIC はトランザクション的ではありません: 障害が発生すると INVALID インデックスが残り、マイグレーション自体はそれを示しません。
SELECT c.relname, i.indisvalid, i.indcheckxmin
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname = 'ix_orders_customer_id';
indisvalid = false—DROP INDEX CONCURRENTLY IF EXISTSで削除して再作成するか、REINDEX INDEX CONCURRENTLYを実行します。- 失敗した CIC マイグレーションの再実行。
IF NOT EXISTSがない場合、INVALID インデックスに対して「リレーションは既に存在します」エラーで失敗します。IF NOT EXISTSがあるとさらに悪い: コマンドは INVALID インデックスを黙ってスキップし、マイグレーションは「成功」します。したがって、CIC にはIF NOT EXISTSを記述しないでください: 最初に無効なインデックスを削除してから、再実行します。
インデックスはあるのに、実行計画が Seq Scan
インデックスは有効なのに、プランナが使用しない場合 — indcheckxmin を確認してください(上のクエリ)。true の場合: インデックス作成前に開始されたトランザクションが生存している間、クエリはインデックスを使用しません。原因は UPDATE: テーブルに、インデックスが参照しない HOT バージョンチェーンが残っています。INSERT だけでは発生しません。インデックスを再作成したり、統計情報に原因を求めたりしないでください。古いトランザクションを特定します:
SELECT pid, xact_start, backend_xmin, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xact_start;
古いトランザクションの完了を待つか、終了させます(pg_terminate_backend(pid)) — ユーザーとの事前調整が必要です。リリース前に再現するには: 1つのセッションで repeatable read の開かれたトランザクションでテーブルを読み取り、別のセッションで行を UPDATE して CREATE INDEX を実行します。indcheckxmin が true になります。
マイグレーターに求めること
- 各マイグレーションは自身のトランザクション、適用ログへの書き込みも含む: 失敗した場合は完全にロールバックされ、すでに適用済みのものはそのまま残ります。
- 並行実行のロック(例: advisory lock): 同時に起動したレプリカが同じものを2回適用しないようにします。
- 個別マイグレーションのトランザクションなしモード — ファイル内のマーク(例: 最初の行に
-- migrate:no-transaction)。そのようなファイルはトランザクションを経ずに、1コマンドとして送信されます。代償: 適用とログへの書き込みはアトミックではなくなります。これらの間にプロセスがクラッシュした場合、再実行は「already exists」で失敗します。indisvalidを確認し、有効なインデックスがあれば、手動でログにマイグレーションを記録します。 - データベースに接続せずにペアファイルを作成する機能。