7.5 KiB
Migration 예시
좋은 예시
예시 1. 새 nullable 컬럼 추가 → backfill → default 설정으로 나눈다
ALTER TABLE auth.users
ADD COLUMN status text;
-- 배치/스크립트로 backfill 수행
-- UPDATE auth.users SET status = 'ACTIVE' WHERE status IS NULL;
ALTER TABLE auth.users
ALTER COLUMN status SET DEFAULT 'ACTIVE';
좋은 이유:
- 컬럼 추가를 빠른 additive change로 분리했다
- 과거 데이터 보정과 future default를 분리했다
SET DEFAULT가 기존 row를 바꾸지 않는다는 점을 전제로 설계했다
PostgreSQL은 컬럼 추가가 가능하고, default 변경은 이후 insert/update에만 영향을 주며 기존 row를 바꾸지 않는다고 설명한다.
예시 2. 큰 테이블의 CHECK/FK는 NOT VALID 후 나중에 검증한다
ALTER TABLE ordering.orders
ADD CONSTRAINT ck_orders__amount_positive
CHECK (amount > 0) NOT VALID;
ALTER TABLE ordering.orders
VALIDATE CONSTRAINT ck_orders__amount_positive;
좋은 이유:
- 제약 추가 시점의 긴 full scan 영향을 줄인다
- 새로 들어오는/갱신되는 row에는 바로 제약이 적용된다
- 기존 데이터 검증은 별도 단계로 분리한다
PostgreSQL은 NOT VALID 제약이 기존 row scan을 건너뛰고, 이후 VALIDATE CONSTRAINT로 검증할 수 있으며 validation은 SHARE UPDATE EXCLUSIVE lock으로 수행된다고 설명한다.
예시 3. NOT NULL은 backfill과 증명 단계를 거쳐 올린다
ALTER TABLE auth.users
ADD CONSTRAINT ck_users__status_not_null
CHECK (status IS NOT NULL) NOT VALID;
-- backfill 수행
-- UPDATE auth.users SET status = 'ACTIVE' WHERE status IS NULL;
ALTER TABLE auth.users
VALIDATE CONSTRAINT ck_users__status_not_null;
ALTER TABLE auth.users
ALTER COLUMN status SET NOT NULL;
ALTER TABLE auth.users
DROP CONSTRAINT ck_users__status_not_null;
좋은 이유:
- null 방지 강화를 한 번에 몰아넣지 않았다
- PostgreSQL이 valid CHECK로 null 불가능함을 증명하면
SET NOT NULLscan을 건너뛸 수 있는 점과 잘 맞는다 - 운영 중 영향도를 줄이기 좋은 패턴이다
PostgreSQL은 SET NOT NULL이 보통 table scan을 하지만, valid CHECK가 null 불가능함을 증명하면 scan을 생략할 수 있다고 설명한다.
예시 4. 운영 인덱스는 CONCURRENTLY로 만들고, 필요하면 제약으로 승격한다
CREATE UNIQUE INDEX CONCURRENTLY uq_users__email_idx
ON auth.users (email);
ALTER TABLE auth.users
ADD CONSTRAINT uq_users__email
UNIQUE USING INDEX uq_users__email_idx;
좋은 이유:
- 인덱스 build 중 write block을 줄인다
- 기존 인덱스를 활용해 빠르게 UNIQUE 제약으로 승격한다
- 큰 테이블의 unique 추가에 적합한 공식 경로다
PostgreSQL은 CREATE INDEX CONCURRENTLY가 production environment에 유용하고, 기존 unique index를 UNIQUE/PRIMARY KEY 제약으로 전환할 수 있다고 설명한다.
예시 5. PostgreSQL 11+에서는 상수 default 컬럼 추가를 안전한 후보로 본다
ALTER TABLE auth.sessions
ADD COLUMN source text DEFAULT 'LOCAL';
좋은 이유:
- 상수 default라면 PostgreSQL 11+에서 빠른 metadata 기반 처리 경로에 들어갈 수 있다
- 운영 중 큰 rewrite를 피할 가능성이 높다
- 상수 default라는 점이 분명하다
PostgreSQL 11 release notes와 현재 ALTER TABLE 문서는 non-volatile/default constant 컬럼 추가가 table rewrite를 피할 수 있다고 설명한다.
나쁜 예시
예시 1. 큰 테이블에 volatile default를 바로 추가한다
ALTER TABLE audit.audit_logs
ADD COLUMN created_bucket timestamp with time zone DEFAULT clock_timestamp();
나쁜 이유:
clock_timestamp()는 volatile default다- PostgreSQL은 이런 경우 전체 테이블과 인덱스 rewrite가 일어날 수 있다고 설명한다
- 운영 테이블에서는 매우 위험할 수 있다
공식 문서는 volatile default 컬럼 추가가 rewrite를 유발한다고 설명한다.
예시 2. 운영 인덱스를 일반 CREATE INDEX로 바로 만든다
CREATE INDEX ix_orders__created_at
ON ordering.orders (created_at);
나쁜 이유:
- write를 막을 수 있는 일반 index build를 사용한다
- 운영 중 대형 테이블에는 영향도가 과도할 수 있다
- 이런 경우는
CREATE INDEX CONCURRENTLY를 먼저 검토해야 한다
PostgreSQL은 CREATE INDEX CONCURRENTLY가 write를 막지 않고 production environment에 유용하다고 설명한다. 그 반대 의미로, 일반 build는 운영 중 더 보수적으로 다뤄야 한다.
예시 3. CREATE INDEX CONCURRENTLY를 트랜잭션 블록 안에 넣는다
BEGIN;
CREATE INDEX CONCURRENTLY ix_users__email
ON auth.users (email);
COMMIT;
나쁜 이유:
- PostgreSQL은
CREATE INDEX CONCURRENTLY가 transaction block 안에서 실행될 수 없다고 명시한다 - migration 도구가 이 구분을 지원하지 않으면 배포 시 실패한다
- non-transactional step으로 분리해야 한다
공식 문서는 regular CREATE INDEX는 transaction block 안에서 가능하지만, CREATE INDEX CONCURRENTLY는 불가능하다고 설명한다.
예시 4. backfill 없이 바로 NOT NULL을 건다
ALTER TABLE auth.users
ADD COLUMN status text;
ALTER TABLE auth.users
ALTER COLUMN status SET NOT NULL;
나쁜 이유:
- 기존 row가 null일 가능성을 무시한다
SET NOT NULL은 보통 전체 테이블 scan을 요구한다- 데이터 보정 없이 곧바로 강한 제약을 올리는 구조다
PostgreSQL은 SET NOT NULL이 기존 row에 null이 없어야 하고, 보통 이를 확인하기 위해 전체 테이블을 스캔한다고 설명한다.
예시 5. 타입 변경을 무심코 직접 수행한다
ALTER TABLE billing.payments
ALTER COLUMN amount TYPE numeric(19,4);
나쁜 이유:
- 타입 변경은 기본적으로 rewrite/rebuild 후보다
- 큰 테이블에서는 시간과 디스크 사용량이 매우 커질 수 있다
- 더 안전한 staged migration이 필요한지 먼저 검토해야 한다
PostgreSQL은 타입 변경이 보통 테이블과 인덱스를 rewrite/rebuild하고, 상당한 시간과 최대 거의 두 배 디스크를 일시적으로 요구할 수 있다고 설명한다.
예시 6. 앱 호환성 검증 전에 rename/drop부터 수행한다
ALTER TABLE auth.users
RENAME COLUMN email TO login_id;
ALTER TABLE auth.users
DROP COLUMN username;
나쁜 이유:
- 기술적으로는 가능한 DDL이지만, 애플리케이션/쿼리/배치/운영 스크립트와의 호환성을 즉시 깨뜨릴 수 있다
- backward-compatible rollout 경로가 없다
- 이런 변경은 마지막 cleanup 단계에서만 허용하는 것이 안전하다
PostgreSQL은 rename/drop을 지원하지만, 프로젝트 운영 기준에서는 destructive/비호환 변경으로 본다. 특히 DROP COLUMN은 빠르지만 디스크 공간도 즉시 줄지 않는다.
예시 7. failed concurrent build 뒤 INVALID 인덱스를 방치한다
-- 실패한 CREATE INDEX CONCURRENTLY 이후
-- 아무 조치 없이 배포 종료
나쁜 이유:
- PostgreSQL은 실패한 concurrent build가 INVALID 인덱스를 남길 수 있다고 설명한다
- 이 인덱스는 query에는 안 쓰여도 update overhead는 계속 발생한다
- drop 후 재시도나
REINDEX INDEX CONCURRENTLY같은 정리 절차가 필요하다
공식 문서는 invalid index가 남을 수 있고, 권장 복구 방법은 drop 후 다시 시도하거나 concurrent reindex라고 설명한다.