-- ❌ 직접 NOT NULL — 대형 테이블 long lock
ALTERTABLEordersALTERCOLUMNstatusSETNOTNULL;-- ✅ 단계별
-- 1) DEFAULT + NOT VALID CHECK
ALTERTABLEordersADDCONSTRAINTorders_status_chkCHECK(statusISNOTNULL)NOTVALID;-- 2) 기존 행 backfill
UPDATEordersSETstatus='pending'WHEREstatusISNULL;-- 3) constraint validate (read lock 만)
ALTERTABLEordersVALIDATECONSTRAINTorders_status_chk;-- 4) 진짜 NOT NULL (Postgres 12+ — fast path)
ALTERTABLEordersALTERCOLUMNstatusSETNOTNULL;ALTERTABLEordersDROPCONSTRAINTorders_status_chk;
Index 추가 — CONCURRENTLY (Postgres)
CREATEINDEXCONCURRENTLYidx_users_emailONusers(email);-- write block 안 함. 단 transaction 안에서는 못 씀.
큰 테이블 backfill — batch
-- 한 번에 1000행씩
DO$$DECLAREbatchINT:=1000;BEGINLOOPWITHidsAS(SELECTidFROMordersWHEREnew_fieldISNULLLIMITbatchFORUPDATESKIPLOCKED)UPDATEordersSETnew_field=compute(id)WHEREidIN(SELECTidFROMids);EXITWHENNOTFOUND;PERFORMpg_sleep(0.1);ENDLOOP;END$$;
🤔 의사결정 기준
변경
안전
Column 추가 (nullable)
✅ 즉시
Column 추가 + DEFAULT (Postgres 11+)
✅ — fast path
NOT NULL 추가
expand-contract
Column rename
expand-contract
Type change (varchar → text)
보통 안전 — but 큰 테이블이면 점검
Type change (int → bigint)
expand-contract
Drop column
expand-contract — 코드 먼저 안 쓰게
Index 추가
CONCURRENTLY
FK 추가
NOT VALID + VALIDATE
❌ 안티패턴
schema 변경과 코드 변경 한 deploy: 둘이 정확히 동시 시작 못 함. 한 쪽만 적용된 짧은 시간 = 사고.
online migration 없는 큰 테이블 ALTER: 수십 분 lock → 다운타임.
rollback 계획 없음: 새 코드 + 새 schema deploy 후 문제 → 옛 코드 + 새 schema 인스턴스 발생 → 사고. expand-contract 면 자연 롤백.
production 에 직접 SQL: history / review 없음. migration tool (Flyway / Prisma migrate / golang-migrate / dbmate) 사용.
migration 안에 비즈니스 로직: 한 트랜잭션에 무거운 변환. 별도 backfill batch.
trigger 가 영구 남음: contract 단계에서 trigger 제거 잊음.
dev 에서만 테스트: 데이터 양이 다름. staging with prod-size dataset 검증.
🤖 LLM 활용 힌트
"schema 변경 = expand-migrate-contract 3 deploy" 강제.