開発・技術選定

Django のマイグレーションで本番を止めない — 事故る変更と、その避け方

開発環境では一瞬で終わるマイグレーションが、本番では数分テーブルをロックする。
これが原因の障害を何度か見てきました。事前に知っておくと避けられます。

危ない変更

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を事前に読む
  • 適用直前にダンプを取る