※ PostgreSQL: ALTER TABLE ... .
안녕하세요. 듀스트림입니다.
오늘 포스팅은 PostgreSQL에서 ALTER TABLE ... (특히 ALTER COLUMN)을 할 때, 내부적으로 어떤 일이 일어나는지에 대한 내용입니다.
운영하다 보면 그냥 한번씩 별일이 있습니다.
먼저, REWRITE에 대해 알아야합니다.
1. REWRITE란 무엇이며, 왜 위험할까요?
- Rewirte(테이블 재작성): 테이블의 새 사본을 만들어 데이터를 모두 재기록한 뒤 원자적으로 교체하는 동작입니다.
이때 ACCESS EXCLUSIVE 급의 강한 락이 걸리고, 일시적으로 테이블 크기만큼 추가 디스크가 필요할 수 있습니다. - MVCC 주의: Rewrite형 ALTER TABLE은 MVCC-세이프가 아니어서, 커밋 직후 이전 스냅샷을 들고 있는 트랜잭션에서 테이블이 비어 보일 수 있습니다(정상 동작). 새 스냅샷부터는 정상입니다. (PostgreSQL)
2. 어떤 ALTER가 REWRITE를 유발할까요?
| 변경 | 테이블 재작성 | 인덱스 재구축 | 설명 |
| ADD COLUMN (DEFAULT 없음/상수 DEFAULT) | 아니오 | 아니오 | 상수 DEFAULT 또는 DEFAULT가 없으면 Rewrite 아님. (PG11 도입) |
| ADD COLUMN (VOLATILE DEFAULT, STORED GENERATED, IDENTITY, “제약 있는 도메인”) | 예 | 경우에 따라 | 명시적 Rewrite 대상. VIRTUAL GENERATED는 예외(Rewrite 아님). |
| DROP COLUMN | 아니오 | 아니오 | "컬럼은 논리적으로 숨겨지고 공간은 추후 REWRITE/VACUUM FULL/CLUSTER로 회수." |
| ALTER COLUMN TYPE (일반적) | 예 | 예(예외있음) | "보통 테이블·인덱스 Rewrite." |
| ALTER COLUMN TYPE (예외: binary-coercible, USING 없이 내용 불변) | 아니오 | 경우에 따라 | "테이블 Rewrite는 생략되나, 인덱스는 재구축될 수 있음." |
| PK/UNIQUE 추가·삭제 | 아니오 | 예 | "PK/UNIQUE는 유니크 인덱스 생성/삭제." |
| NOT NULL/ CHECK 추가 | 아니오(단, 검증 스캔 필요) | 아니오 | NOT VALID/VALIDATE로 분리 가능, 검증 시 락 완화. |
| COLLATION 변경 | - | REINDEX 필요 | 콜레이션 변경 시 의존 객체 재구축 권고. |
| 파티션 구조 변경 제약 | - | - | 부모·파티션 열 일치 필요 등 제약. |
3. 케이스별 상세 내용
3.1 ADD COLUMN: Rewrite 트리거
- 빠른 경로: 상수 DEFAULT 또는 DEFAULT 없음 → Rewrite 없음. 기존 행은 읽을 때 기본값/NULL을 논리적으로 적용합니다. (PG11부터 최적화)
- Rewrite 유발: DEFAULT clock_timestamp() 같은 VOLATILE, GENERATED … STORED, IDENTITY, 제약 있는 도메인 → 전체 테이블 & 인덱스 Rewrite. VIRTUAL GENERATED는 예외(Rewrite 없음).
- 복제 엣지(참고): PG 15.6 릴리스 노트에는 상수 DEFAULT 최적화가 논리복제 초기 동기화에서 NULL 전송 이슈를 일으킬 수 있었음을 수정했다고 기록되어 있습니다. 대규모 DDL/복제 환경에선 반드시 버전/패치 수준 확인이 필요합니다.
3.2 DROP COLUMN: 논리 삭제와 공간 회수
- DROP COLUMN은 데이터 파일을 바로 줄이지 않습니다.
- 컬럼을 숨김 처리하며(pg_attribute 관점), 공간은 VACUUM FULL/CLUSTER/REWRITE형 ALTER로 회수합니다.
3.3 ALTER COLUMN TYPE: 일반 원칙과 예외
- 일반 원칙: 테이블·인덱스 Rewrite가 "보통" 필요합니다. USING으로 값 변환이 있거나 내부 저장 포맷이 바뀌면 100% 그렇습니다.
- 예외(핵심): 기존 타입이 새 타입에 binary-coercible 이고, USING 없이 값이 바뀌지 않으면 테이블 Rewrite는 생략됩니다(인덱스는 재구축될 수 있음). → varchar(50)→varchar(100), text↔varchar 같은 케이스가 여기에 해당합니다.
- Default와 USING의 상호작용: USING은 기존 DEFAULT에 적용되지 않을 수 있어, 필요 시 기존 DEFAULT를 먼저 DROP 후 타입 변경, 이후 새 DEFAULT를 설정하는 절차를 따릅니다.
3.4 CONSTRAINT : 검증 스캔, 락 완화
- NOT NULL / CHECK 추가는 테이블 스캔이 필요하지만 Rewrite는 아닙니다.
- 대형 테이블이면 NOT VALID → VALIDATE CONSTRAINT 로 나눠 락을 완화할 수 있습니다.
- VALIDATE는 대상 테이블에 SHARE UPDATE EXCLUSIVE 정도로 수행됩니다. (외부 FK는 ROW SHARE 추가 요구)
3.7 PK/UNIQUE/INDEX/COLLATION
- PK/UNIQUE 추가는 유니크 인덱스 생성(테이블 Rewrite 없음), 삭제는 해당 인덱스 제거입니다.
- 컬럼 타입 자체를 바꾸면 그 변경 규칙을 따르므로 Rewrite가 필요할 수 있습니다.
- 콜레이션 변경·업그레이드가 있으면 의존 객체(특히 인덱스)를 REINDEX 해야 합니다.
3.6 파티션/상속과 컬럼/제약의 정합
- 파티션 테이블은 부모와 컬럼 구성이 동일해야 하며, CHECK/NOT NULL은 상속 규칙이 적용됩니다.
- 일부 제약은 부모에만 추가/삭제하는 것이 제한될 수 있습니다.
- 파티션/부모 혼합 환경에서 DDL 적용 전 정합성 규칙을 반드시 확인하세요.
4. LOCK·DISK·MVCC
| 항목 | 설명 |
| 락 수준 | 다수의 ALTER TABLE은 ACCESS EXCLUSIVE 급 락을 사용(동시 읽기/쓰기 차단). |
| 디스크 공간 | REWRITE/VACUUM FULL/CLUSTER 는 테이블 크기 상당의 임시 공간을 필요로 할 수 있음. |
| MVCC Caveats | TRUNCATE/REWRITE형 ALTER는 NOT MVCC-SAFE → 커밋 직후 이전 스냅샷 트랜잭션에서 빈 테이블처럼 보일 수 있음. |
5. 런북
5.1 사전 체크리스트
- 변경 분류: 위 결정표로 리라이트 여부와 인덱스 재구축 여부를 판정. (특히 VOLATILE DEFAULT/GENERATED STORED/IDENTITY/도메인 제약/타입 변경)
- 락 허용도: 비즈니스 창구에서 ACCESS EXCLUSIVE 허용 가능한가? 대기/블로킹 시나리오를 정리.
- 디스크 여유: 리라이트/VACUUM FULL/CLUSTER 시 일시적 2배 가까운 공간이 필요한지 점검.
- 검증 전략: NOT VALID → VALIDATE 로 나눌 수 있는지, 검증 스캔 시간·락 수준을 계획.
- 콜레이션/인덱스: 콜레이션 영향이 있으면 REINDEX 계획 수립.
- 복제/서브스크라이버: 논리복제/리플리케이션 동작과 버전 이슈 확인.
5.2 적용 예시
▸ ADD COLUMN
ALTER TABLE t ADD COLUMN status text;
ALTER TABLE t ADD COLUMN status text DEFAULT 'active'; -- 상수 DEFAULT
→ Rewrite 없음. (대량 업데이트 예정이라면 DEFAULT 없이 추가 후 UPDATE/SET DEFAULT 순서도 고려)
▸ ADD COLUMN(Rewrite)
ALTER TABLE t ADD COLUMN created_at timestamptz DEFAULT clock_timestamp(); -- VOLATILE
→ Rewrite. (서비스 창구 확보 및 스토리지·락 점검 필수)
▸ ALTER TYPE(예외 케이스)
ALTER TABLE t ALTER COLUMN name TYPE varchar(200); -- AS-IS varchar(50)
→ binary-coercible + USING X → 테이블 Rewrite 생략(인덱스는 상황 따라 재구축).
▸ DROP COLUMN + VACUUM FULL
ALTER TABLE t DROP COLUMN old_col; -- DROP COLUMN은 공간 회수를 하지 않음
VACUUM FULL t; -- Rewrite를 통해 공간 회수
▸ ADD CONSTRAINT
ALTER TABLE t ADD CONSTRAINT t_chk NOT VALID CHECK (amount >= 0);
ALTER TABLE t VALIDATE CONSTRAINT t_chk; -- SHARE UPDATE EXCLUSIVE 수준
5.3 중단(취소/오류/크래시) 시 동작
- ALTER TABLE은 트랜잭션 내에서 실행됩니다.
- 커밋 전 중단이면 전부 롤백되어 원본 유지 → 새 파일은 폐기되고, 원본 파일(relfilenode)은 그대로 유지됩니다. (Rewrite는 Atomic swap 모델)
- 중단/실패 시 롤백 과정에서는 MVCC Caveats이 발생하지 않습니다.
- Rewrite형 DDL은 커밋 직후 MVCC Caveats이 발생하여, 이전 스냅샷에서 테이블이 빈 것처럼 보일 수 있습니다.
MVCC Caveats?
아직 커밋 이전의 스냅샷을 들고 있는 다른 트랜잭션이 "이 테이블의 relfilenode가 바뀌었지만, 자신은 새 버전을 모르는 상태"이기 때문에 해당 세션에서 새 파일을 못 보고 빈 테이블처럼 인식하는 것입니다.
- 요약
| 단계 | relfilenode 상태 | MVCC 영향 | 테이블 가시성 |
| Rewrite 중 (데이터 복사 진행 중) |
새 relfilenode 생성, 커밋 전 | 없음 | 다른 트랜잭션은 여전히 원본 파일을 읽음 |
| 중단/롤백 시 | 새 파일 삭제, 원본 유지 | 없음 | 원본 그대로 유지 |
| 커밋 직후 | relfilenode 교체 완료 | 있음 (기존 스냅샷만 비어 보임) | 새 스냅샷에서는 정상 |
| 커밋 이후 새로운 트랜잭션 | 새 파일 읽음 | 없음 | 정상 |
5.4 사후 작업
- ANALYZE: 타입 변경 후 해당 컬럼의 통계가 제거되므로, 한번 수행해 주시는 게 좋습니다.
- REINDEX: 콜레이션 작업 시 세트로 생각하시면 됩니다.
그래서 초기 설계가 중요합니다. 운영 중 변경은 리스크도 크고 힘들어요.
아래는 참고용 공식 문서입니다.
ALTER TABLE
ALTER TABLE ALTER TABLE — change the definition of a table Synopsis ALTER TABLE [ IF EXISTS ] [ ONLY …
www.postgresql.org
5.7. Modifying Tables
5.7. Modifying Tables # 5.7.1. Adding a Column 5.7.2. Removing a Column 5.7.3. Adding a Constraint 5.7.4. Removing a Constraint 5.7.5. …
www.postgresql.org
아.. 그리고 이게 진짜 중요한데... Rewrite 발생 시 테이블/인덱스 사이즈에 따라 대량의 WAL이 생성됩니다.
모든 영속적 변경은 WAL에 기록됩니다.
오늘은 여기까지~
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: Heap Table Structure (0) | 2025.11.05 |
|---|---|
| PostgreSQL: Heap Fetch (0) | 2025.11.03 |
| PostgreSQL: 캐시 히트 (0) | 2025.10.29 |
| PostgreSQL: Merge Join (0) | 2025.10.22 |
| PostgreSQL: ERROR [40001]: canceling statement due to conflict with recovery (0) | 2025.10.20 |