Files
2nd/10_Wiki/Topics/Coding/DB_Migration_Safety.md
T
2026-05-09 21:08:02 +09:00

4.4 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-migration-safety DB Migration Safety — Zero-downtime 전환 패턴 Coding draft B conceptual 2026-05-09 2026-05-09
database
migration
zero-downtime
postgres
vibe-coding
language applicable_to
SQL / Postgres / MySQL
Backend
expand-contract
blue-green schema
online migration
lock-free

DB Migration Safety

Expand → Migrate → Contract 3단계가 정답. 한 번에 schema + 코드 둘 다 못 바꾼다 — 둘이 동시에 deploy 안 되니까. NOT NULL 추가 / column rename / type change 가 가장 위험.

📖 핵심 개념

  • Expand: 새 schema 추가. 옛 코드는 옛 schema 만 사용. 양립 가능.
  • Migrate: 코드를 새 schema 로 이전. dual-write 또는 backfill.
  • Contract: 옛 schema 제거.
  • 각 단계는 별도 deploy. 사이에 모니터링 시간.

💻 코드 패턴

Column rename old_name → new_name

Expand

ALTER TABLE users ADD COLUMN new_name TEXT;
-- Trigger: old_name 변경 시 new_name 동기화
CREATE OR REPLACE FUNCTION sync_name() RETURNS trigger AS $$
BEGIN NEW.new_name := COALESCE(NEW.new_name, NEW.old_name); RETURN NEW; END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_name BEFORE INSERT OR UPDATE ON users
  FOR EACH ROW EXECUTE PROCEDURE sync_name();

-- Backfill (낮 시간대 batch)
UPDATE users SET new_name = old_name WHERE new_name IS NULL;

Migrate (코드 deploy)

  • 코드: 읽기는 COALESCE(new_name, old_name), 쓰기는 둘 다.

Contract

DROP TRIGGER trg_sync_name ON users;
DROP FUNCTION sync_name();
ALTER TABLE users DROP COLUMN old_name;

NOT NULL 추가 — 큰 테이블

-- ❌ 직접 NOT NULL — 대형 테이블 long lock
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;

-- ✅ 단계별
-- 1) DEFAULT + NOT VALID CHECK
ALTER TABLE orders ADD CONSTRAINT orders_status_chk CHECK (status IS NOT NULL) NOT VALID;
-- 2) 기존 행 backfill
UPDATE orders SET status = 'pending' WHERE status IS NULL;
-- 3) constraint validate (read lock 만)
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_chk;
-- 4) 진짜 NOT NULL (Postgres 12+ — fast path)
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
ALTER TABLE orders DROP CONSTRAINT orders_status_chk;

Index 추가 — CONCURRENTLY (Postgres)

CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- write block 안 함. 단 transaction 안에서는 못 씀.

큰 테이블 backfill — batch

-- 한 번에 1000행씩
DO $$
DECLARE batch INT := 1000;
BEGIN
  LOOP
    WITH ids AS (
      SELECT id FROM orders WHERE new_field IS NULL LIMIT batch FOR UPDATE SKIP LOCKED
    )
    UPDATE orders SET new_field = compute(id) WHERE id IN (SELECT id FROM ids);
    EXIT WHEN NOT FOUND;
    PERFORM pg_sleep(0.1);
  END LOOP;
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" 강제.
  • 큰 테이블 ALTER 는 항상 단계별 + CONCURRENTLY 권장.

🔗 관련 문서