開発環境では一瞬で終わるマイグレーションが、本番では数分テーブルをロックする。
これが原因の障害を何度か見てきました。事前に知っておくと避けられます。
危ない変更
1. NOT NULL のカラムを既存テーブルに追加
status = models.CharField(max_length=20) # デフォルト無し = 危険
既存行すべてに値を書き込むため、行数に比例して時間がかかります。
安全なのは2段階に分ける方法です。
段階1: null=True で追加(一瞬で終わる)
段階2: バッチで値を埋める
段階3: NOT NULL 制約を付ける
2. インデックスの追加
大きなテーブルではロックが長くなります。PostgreSQL なら並行作成が使えます。
from django.contrib.postgres.operations import AddIndexConcurrently
class Migration(migrations.Migration):
atomic = False # ← これが必須
operations = [AddIndexConcurrently(...)]
atomic = False を忘れるとエラーになります。
3. カラムの削除・リネーム
デプロイ中は「新旧のコードが同時に動く」瞬間があります。
先にカラムを消すと、旧コードが 500 を返します。
デプロイ1: コードから参照を消す(カラムは残す)
デプロイ2: カラムを削除する
必ずコードを先、スキーマを後にします。
事前に確認する
適用されるSQLを、実行前に必ず目で見ます。
python manage.py sqlmigrate myapp 0012
ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT のような
重い操作が含まれていないかを確認します。
本番適用の手順
# 1. 直前にダンプを取る(切り戻せる状態を作る)
pg_dump -Fc mydb > backup_$(date +%Y%m%d_%H%M).dump
# 2. 適用
python manage.py migrate --noinput
# 3. 確認
python manage.py showmigrations | grep '\[ \]'
戻せない変更(カラム削除など)の前は、必ずダンプを取ってからです。
まとめ
- NOT NULL 追加は3段階に分ける
- インデックスは並行作成(
atomic = False) - カラム削除はコードを先、スキーマを後
sqlmigrateで実行SQLを事前に読む- 適用直前にダンプを取る