データ移行はコード変更より戻しにくい
アプリケーションコードは、デプロイを戻せば元に戻せることが多いです。しかし、DBのデータを消したり、上書きしたり、型を変えたりした後は簡単に戻せません。
データ移行では、既存データを失わず、新旧アプリが共存でき、途中失敗しても再開・検証できることが重要です。
特に本番DBでは、移行SQLを一発で流す発想を避けます。
危険な変更
注意が必要な変更です。
| 変更 | 危険 |
|---|---|
| カラム削除 | 既存アプリやバッチが読めなくなる |
| カラム名変更 | 新旧アプリが共存できない |
| 型変更 | 変換失敗やロックが起きる |
| NOT NULL追加 | 既存NULLで失敗、ロックが起きる |
| UNIQUE追加 | 重複データで失敗 |
| 大量UPDATE | ロック、WAL増加、レプリカ遅延 |
| テーブル分割 | 移行漏れや整合性崩れ |
危険な変更ほど、段階を分けます。
expand / contract
安全なDB変更では、expand / contract という考え方を使います。
expand: 互換性を保ったまま新しい構造を追加する
contract: 移行完了後に古い構造を削除する
例: name を first_name と last_name に分ける
Step 1: 新カラム追加
ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name TEXT;
Step 2: アプリを新旧両方に対応
旧nameも読む
新first_name/last_nameも書く
Step 3: バックフィル
UPDATE users
SET first_name = split_part(name, ' ', 1),
last_name = split_part(name, ' ', 2)
WHERE first_name IS NULL;
Step 4: 新アプリへ切り替え
Step 5: 旧カラム削除
ALTER TABLE users DROP COLUMN name;
削除は最後です。
バックフィルは小さく刻む
大量データを一括UPDATEすると、ロックや負荷が大きくなります。
避けたい例:
UPDATE orders
SET status = 'paid'
WHERE paid_at IS NOT NULL;
安全寄りの考え方:
主キー順に1000件ずつ処理
処理済みIDを記録
途中失敗しても再開できる
短いトランザクションで実行
例:
UPDATE orders
SET status = 'paid'
WHERE id > $1
AND id <= $2
AND paid_at IS NOT NULL
AND status IS NULL;
条件に status IS NULL を入れると、再実行しても同じ行を壊しにくくなります。移行処理は冪等に近づけます。
二重書き込み
新旧カラムや新旧テーブルが共存する期間は、二重書き込みが必要になることがあります。
新規作成時:
old_column に書く
new_column にも書く
更新時:
old_column を更新
new_column も更新
これにより、移行期間中に新しく入ったデータも取りこぼしにくくなります。
注意点:
- どちらを正とするか決める
- 書き込み失敗時の扱いを決める
- バックフィル後の差分を検証する
- 二重書き込み期間を長くしすぎない
DBトリガーで同期する方法もありますが、アプリ側から見えにくくなるため、運用とチームの理解が必要です。
検証SQLを先に用意する
移行は、実行SQLだけでなく検証SQLが重要です。
例: 件数確認
SELECT COUNT(*) FROM users WHERE name IS NOT NULL;
SELECT COUNT(*) FROM users WHERE first_name IS NOT NULL;
例: 移行漏れ
SELECT id
FROM users
WHERE name IS NOT NULL
AND first_name IS NULL
LIMIT 100;
例: 重複確認
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
検証できない移行は危険です。どの状態になったら成功と言えるのかを、移行前に決めます。
ロールバックではなくロールフォワードも考える
DB変更では、単純なロールバックが難しいことがあります。
例:
- カラム削除後にデータが消えた
- 型変換で元の文字列が失われた
- 新旧で書き込みが分岐した
そのため、戻すだけでなく、前に進めて直すロールフォワードも考えます。
問題発生
-> 旧カラムが残っているなら旧アプリへ戻す
-> 移行漏れなら追加バックフィル
-> 不整合なら補正SQL
削除や破壊的変更を最後にするのは、ロールバック余地を残すためです。
本番前に見ること
実行前チェック:
- バックアップがある
- リストア手順を確認した
- ステージングで本番相当データを試した
- 実行時間を測った
- ロックの影響を見た
- レプリカ遅延を想定した
- アプリの新旧バージョン共存を確認した
- 検証SQLを用意した
- 中断と再開手順を用意した
- 監視項目を決めた
「SQLは正しい」だけでは足りません。本番で安全に流せるかを確認します。
まとめ
データ移行は、既存データを守りながら構造を変える作業です。
- 削除や破壊的変更を急がない
- expand / contractで段階的に進める
- バックフィルは小さく刻む
- 二重書き込み期間を設計する
- 検証SQLを先に用意する
- ロールバックとロールフォワードを考える
- バックアップと監視を用意する
DB移行がうまいエンジニアは、変更そのものより「失敗してもデータを失わない道筋」を先に作ります。