TL;DR
- DBマイグレーション事故の7割は「NOT NULLカラム追加」「インデックス追加時のロック」「NOT IN問題」の3パターンに集約される。
- ゼロダウンタイムの基本は expand-contract pattern(Expand → Contract → Cleanup の3フェーズ展開)。
- ロールバック設計を先に書く — 本番投入前に「どうやって戻すか」が決まっていない変更は実施しない。
はじめに
こんにちは、みねです。
「本番でマイグレーション実行したら意図しないテーブルロックが発生した」「ロールバックしようとしたら戻せなかった」「NOT NULLカラムを追加したら既存アプリがクラッシュした」——この種の事故は、DB設計の知識があるエンジニアでも踏む。
原因の多くはマイグレーション変更の安全設計ではなく、その欠如にある。本記事では代表的な事故パターンを整理し、明日から使える安全設計の手順を具体的に解説する。
事故パターン一覧
| パターン | 主な原因 | 影響 | 発生しやすい操作 |
|---|---|---|---|
| LOCK地獄 | テーブル全体に排他ロック | 読み書き停止・タイムアウト連鎖 | ALTER TABLE ADD COLUMN NOT NULL, CREATE INDEX |
| backward incompatibility | 旧アプリが新スキーマを読めない | 500エラー・データ破損 | カラム名変更、型変更、NOT NULL追加 |
| ロールバック不可 | データ変換が不可逆 | 消えたデータは戻らない | DROP COLUMN, UPDATE 系データ変換 |
| 巨大テーブルの長時間ロック | 全行スキャンが必要な変更 | 数時間の書き込み停止 | インデックス追加、カラム型変更 |
| NOT IN問題 | NULL が含まれる列での NOT IN | クエリが常にゼロ行を返す | 新カラム追加後のクエリ移行 |
LOCK地獄の詳細
PostgreSQL の ALTER TABLE はデフォルトで AccessExclusiveLockを取得する。これはすべての読み書きをブロックする最強ロックで、NOT NULLデフォルト値なしのカラム追加では全行書き換えが走る。
-- 危険: 本番テーブルで実行すると全行ロック
ALTER TABLE orders ADD COLUMN status VARCHAR(50) NOT NULL DEFAULT 'pending';
-- PostgreSQL 11+ では DEFAULT 付き NOT NULL は行書き換えなし
-- ただし CHECK constraint の検証は走るため大テーブルでは要注意
インデックス追加も同様。CREATE INDEX はデフォルトで共有ロックを取得し、書き込みをブロックする。
-- 危険: 書き込みをブロック
CREATE INDEX idx_orders_status ON orders(status);
-- 安全: CONCURRENTLY でロックなし追加(PostgreSQL)
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);
CONCURRENTLYオプションは2回のテーブルスキャンで完結するため処理が長くなるが、本番では原則これを使う。
backward incompatibility の詳細
「DBスキーマを先にデプロイして、アプリは後から更新する」という順序を取ったとき、旧アプリが新スキーマに対応できない期間が生じる。
典型的な失敗例:
-- スキーマ変更: user_name を full_name にリネーム
ALTER TABLE users RENAME COLUMN user_name TO full_name;
# 旧アプリのコード (まだデプロイされていない)
user_name = row['user_name'] # KeyError が発生する
カラムリネームは旧アプリとの互換性を即座に破壊する。後述の expand-contract pattern で安全に移行する。
NOT IN 問題の詳細
NULL を含む列に NOT IN を使うと、クエリが意図通りに動かない。
-- orders テーブルに nullable な partner_id カラムを追加した後
SELECT * FROM orders WHERE partner_id NOT IN (1, 2, 3);
-- partner_id が NULL の行は返らない(NULL との比較は常に UNKNOWN)
新カラム追加後にクエリを書き換える際は NOT EXISTS か IS NULL 条件を明示する。
-- 修正後
SELECT * FROM orders
WHERE partner_id IS NULL
OR partner_id NOT IN (1, 2, 3);
expand-contract pattern の実装
expand-contract pattern(別名: parallel change pattern)は、後方互換性を保ちながら段階的にスキーマを変更する手法。3フェーズで実施する。
フェーズ1: Expand(拡張)
新しいカラム・テーブルを追加し、旧アプリが動作したまま新しい構造を並存させる。
-- フェーズ1: full_name カラムを追加(user_name はまだ残す)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
-- アプリ側: 書き込み時に両方のカラムに書く
UPDATE users SET full_name = user_name WHERE full_name IS NULL;
# アプリ側の書き込み(両カラムに書く過渡期コード)
user.user_name = name
user.full_name = name
db.commit()
チェックポイント: 旧アプリが user_name を読んで壊れないことを確認してからフェーズ2へ。
フェーズ2: Contract(収縮)
新しいカラムへの移行が完了したことを確認し、アプリを新スキーマのみに切り替える。
# アプリ側: full_name のみ参照する新コードをデプロイ
full_name = user.full_name
-- バックフィル: 移行漏れがないかチェック
SELECT COUNT(*) FROM users WHERE full_name IS NULL;
-- 0 行であることを確認してから次へ
チェックポイント: 全インスタンスが新コードで動作中であることを確認。
フェーズ3: Cleanup(削除)
旧カラムを安全に削除する。この段階で初めて後方互換性の拘束がなくなる。
-- フェーズ3: 旧カラムを削除
ALTER TABLE users DROP COLUMN user_name;
:::message
DROP COLUMN 後は物理的なデータ削除のタイミングが DB によって異なる。PostgreSQL では即時削除されないが、VACUUM FULL まで領域は解放されない。
:::
ロールバック設計の作り方
ロールバック設計は「マイグレーション実行前に書く」が鉄則。本番事故が起きてから考えると時間を無駄にする。
フェーズごとのロールバック方法
| フェーズ | 正方向の変更 | ロールバック方法 | 難易度 |
|---|---|---|---|
| Expand | カラム追加 | DROP COLUMN(データなし or バックアップ) | 低 |
| Expand | カラム追加(データ移行済み) | DROP COLUMN + 旧コードに戻す | 中 |
| Contract | アプリ切り替え | 旧バージョンのアプリをデプロイ | 低 |
| Cleanup | カラム削除 | バックアップからリストア | 高(事実上不可) |
Cleanup フェーズのロールバックは困難。DROP COLUMN 前に必ずバックアップを取るか、以下のような安全弁を用意する。
-- 削除前: 別テーブルにデータをバックアップ
CREATE TABLE users_user_name_backup AS
SELECT id, user_name FROM users;
-- 削除後にロールバックが必要になった場合
ALTER TABLE users ADD COLUMN user_name VARCHAR(255);
UPDATE users u
SET user_name = b.user_name
FROM users_user_name_backup b
WHERE u.id = b.id;
ロールバック判断のフロー
本番実行 → エラー率/レイテンシを5分監視
↓ 正常
次フェーズへ進む
↓ 異常(エラー率 > 1% or P99 latency 2倍超)
ロールバックスクリプトを実行(事前に用意済み)
↓
インシデント記録 → 根本原因調査 → 再計画
online DDL と feature flag の使い分け
online DDL(PostgreSQL / MySQL)
online DDL はロックを最小化しながら DDL を実行する仕組み。PostgreSQL では CONCURRENTLY オプション、MySQL では ALGORITHM=INPLACE や ALGORITHM=INSTANT がある。
-- PostgreSQL: インデックスをロックなしで追加
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);
-- MySQL 8.0+: 即時追加(カラム追加は一部 INSTANT 対応)
ALTER TABLE orders ADD COLUMN note TEXT, ALGORITHM=INSTANT;
online DDL が使えない操作(PostgreSQL):
- プライマリキーの変更
NOT NULL制約の追加(既存 NULL データがある場合)- カラム型の変更(一部)
これらは expand-contract pattern と組み合わせて段階的に対処する。
feature flag との組み合わせ
データ構造の変更とアプリコードの切り替えを別々にコントロールするために feature flag を使う。
# フェーズ1完了後: フラグで新旧コードを切り替え
if feature_flags.is_enabled("use_full_name"):
name = user.full_name
else:
name = user.user_name
feature flag のメリット:
- 即時ロールバック: コードの再デプロイなしにフラグを切るだけで戻せる
- 段階的ロールアウト: カナリア比率を変えながら影響を観測できる
- 本番テスト: 一部ユーザーで動作を確認してから全体展開
feature flag の注意点:
- フラグが残り続けると負債になる → 展開完了後に必ず削除
- フラグ評価のオーバーヘッドはホットパスでは測定必須
安全なマイグレーションチェックリスト
実行前チェック
- ロールバックスクリプトを事前に書いた(
down.sqlまたはrollback.sh) - ステージング環境で動作確認済み(本番と同等のデータ量で検証)
- 実行時間を計測した(ステージングでの実測値をもとに本番投入時間を見積もった)
- ロック取得時間を確認した(
EXPLAIN/pg_stat_activityで確認) - インデックス追加は
CONCURRENTLYを使っている - NOT NULL カラムは
DEFAULT値を設定している(PostgreSQL 11+ なら行スキャンなし) - backward compatible か確認した(旧アプリと新スキーマの組み合わせで動くか)
- カラム削除前にバックアップを取った
- feature flag またはデプロイ順序を定義した
実行中チェック
-
pg_stat_activityでロック待ちがないか監視している - エラーレートを監視している(Datadog / CloudWatch 等)
- P99 レイテンシを監視している
- ロールバック判断の閾値を決めている(「エラー率 X% を超えたら即ロールバック」)
- 作業ログをインシデント対応チャンネルに投下している
実行後チェック
- データ整合性チェッククエリを実行した(行数・NULL数・制約違反がないか)
- アプリのエラーログに異常がないか確認した
- 不要な feature flag を削除した
- 一時バックアップテーブルのクリーンアップ計画を立てた
FAQ
Q1. PostgreSQL で大量データのバックフィルを本番停止なしに実施するには?
A. バッチで少量ずつ更新するアプローチが定番です。一括 UPDATE は全行にロックをかけるため、以下のようにIDレンジで小分けにする。
-- 1000件ずつバッチ更新(PostgreSQL)
DO $$
DECLARE
batch_size INT := 1000;
max_id BIGINT;
current_id BIGINT := 0;
BEGIN
SELECT MAX(id) INTO max_id FROM users;
WHILE current_id < max_id LOOP
UPDATE users
SET full_name = user_name
WHERE id > current_id
AND id <= current_id + batch_size
AND full_name IS NULL;
current_id := current_id + batch_size;
PERFORM pg_sleep(0.1); -- 負荷調整
END LOOP;
END $$;
pg_sleep(0.1) で各バッチ間に100msの休止を入れることで、レプリケーションラグや I/O 負荷を抑制できる。
Q2. マイグレーションツール(Flyway / Liquibase / Alembic)はロールバックをサポートしていますか?
A. ツールによって異なります。
| ツール | ロールバック | 備考 |
|---|---|---|
| Flyway (Community) | 非対応 | Teams 版は undo マイグレーションをサポート |
| Liquibase | rollback コマンドあり | <rollback> タグを事前に記述が必要 |
| Alembic | downgrade コマンドあり | down() 関数の実装が必要 |
| golang-migrate | down コマンドあり | *.down.sql の事前作成が必要 |
どのツールでもロールバック用のスクリプトを事前に書く習慣が最重要。ツール任せにせず、ロールバックが機能することをステージングで検証する。
Q3. blue-green デプロイ環境ではDBマイグレーションをどう扱うべきですか?
A. blue-green ではスキーマ変更が常に backward compatible であることが前提になります。blue と green の両バージョンが同じDBを参照するため、旧バージョン(blue)が新スキーマを読んでも壊れないよう設計する必要があります。
具体的な手順:
- Expand フェーズで新カラムを追加(旧バージョンは無視するだけ)
- green をデプロイして動作確認
- トラフィックを green に切り替え(blue はスタンバイ)
- blue への切り戻しが不要になったら Cleanup フェーズでカラム削除
この手順を守れば、緊急時に blue への即時フェイルバックも可能。Cleanup フェーズの実施は切り戻しが完全に不要になってから行う。
AI 生成 migration の事故 3 パターン(2026-05 追記)
AI コーディング普及で、agent が migration スクリプトを生成する場面が増えました。AI 生成 migration には固有の事故パターンが 3 つあります(経験則)。
パターン A: NOT NULL を意識しない型追加
AI は DDL の構文は正しいが、本番影響の深さを推測できないことがあります(経験則)。ALTER TABLE ... ADD COLUMN col INT NOT NULL を 1 行で書いてしまい、巨大テーブルで全行ロックを引き起こす事故。人間レビューでは「巨大テーブル + NOT NULL」のパターンを必ずチェック。詳細は本記事 §「事故パターン一覧」の NOT NULL 章を参照。
パターン B: index lock のリスクが未考慮
AI が CREATE INDEX を提案する場合、CONCURRENTLY オプションの省略・ロック時間の想定不足がよく発生(経験則)。PostgreSQL では CREATE INDEX CONCURRENTLY[公式値](PostgreSQL 17 公式)、MySQL では pt-online-schema-change / gh-ost[公式値](gh-ost 公式)の利用が必須水準。
パターン C: data backfill の段階分けなし
AI 生成では「カラム追加 → backfill → NOT NULL 化」の 3 段階を 1 つのトランザクションにまとめてしまう傾向(経験則)。expand-contract pattern を Hook で強制する設計が現実解(Claude Code hooks の実践パターン集 参照)。
人間レビューチェックリスト(AI migration 専用)
5 項目(経験則):
- 対象テーブル行数: 100 万行超なら段階分け必須
- NOT NULL / unique 制約: 既存データ全行に対する整合性
- CONCURRENTLY / online DDL の利用: PostgreSQL / MySQL とも公式値の online DDL を使うか
- rollback スクリプト: AI が生成しない場合は手動で書く
- CFR への影響: AI-augmented migration の CFR を分離計測(AI変更後のCFR再定義ガイド 参照)
障害設計 hub への接続
本記事は 障害設計 hub の DB 領域 を担当します。関連 hub:
- マイクロサービスの失敗パターン — サービス分割の失敗
- エラーハンドリング設計ガイド — retry / idempotency / circuit breaker
- AI Change Failure Analysis — AI 変更の事故分析
- AI変更後のCFR再定義ガイド — CFR の AI 時代再定義
- AIコーディング運用インシデントRunbookハブ — 4 種別 Runbook の集約
可観測性スタックは AIコーディング運用の可観測性スタック2026、Edge ランタイムでの DB 接続は Edge Functions採用の判断軸 を参照。
公式仕様 References(2026-05 再検証)
- PostgreSQL 17 公式 - CREATE INDEX —
CONCURRENTLYの挙動 - PostgreSQL 17 公式 - ALTER TABLE — column 追加時のロック仕様
- MySQL 8.4 LTS 公式 - Online DDL — InnoDB Online DDL
- pg_repack — table 再構築ツール
- gh-ost — GitHub の MySQL online schema migration tool
- Liquibase / Flyway — マイグレーション管理ツール
まとめ
DBマイグレーション事故の根本は「ロールバック設計なしの本番投入」と「ロックの見落とし」。expand-contract patternを標準フローにし、ロールバックスクリプトを書いてから実行する習慣が事故の9割を防ぐ。AI 生成 migration の場合は 本記事の人間レビューチェックリスト 5 項目を必ず通すのが現実解(経験則)。
:::message 本番マイグレーションの設計レビューや SRE 支援についてのご相談は、DM または各種問い合わせ窓口からお気軽にどうぞ。 :::
