+ 불확실성을 보존한 단계별 온라인 계획이 탄탄하다.
- 빠른 기본값 경로 대신 불필요한 백필 비용을 만든다.
운영 중 테이블에 걸 때 락이 얼마나 오래 잡히는지 짚습니다. 무중단으로 쪼개는 순서를 줍니다.
| 분류 | 개발 › 데이터·DB |
|---|---|
| 태그 | 검토체크리스트개발자 |
Check this migration for production safety. For each statement: 1. What lock it takes, and whether it blocks reads, writes, or both. 2. Whether the duration scales with table size or is constant. *This is the line between a safe deploy and an outage.* 3. Whether it rewrites the table. 4. Rough duration at my stated table size. 5. Whether it can be done online, and how — typically: add nullable, backfill in batches, add the constraint as not-valid, validate separately. Then: - **Ordered plan** splitting risky statements into separate deploys, with what must ship in the application between them. - The rollback for each step. *A step with no rollback must be identified before it runs, not after.* - Statements that must not run inside a transaction with others. - What to watch during the run, and the abort condition. Rules: - *Behavior differs by database and version. State which engine you are assuming and flag anything version-dependent.* If I did not tell you the version, say what changes between versions. - Assume the table is being written to during the migration. - Do not suggest maintenance downtime as the first answer. Say what it would cost and give the online path too.
로컬에서 1초 걸린 마이그레이션이 운영에서 테이블을 잠근다. 이 프롬프트는 각 구문이 어떤 락을 얼마나 잡는지 판정하고, 위험한 건 여러 단계로 쪼개는 순서를 준다.
ChatGPT가 가장 안전하고 균형적이다. Gemini는 구체적이나 과도한 단언과 위험한 결합이 있고, [C]는 미제공이다.
+ 불확실성을 보존한 단계별 온라인 계획이 탄탄하다.
- 빠른 기본값 경로 대신 불필요한 백필 비용을 만든다.
+ 락·인덱스 위험과 운영 감시를 구체화했다.
- 미확인 타입 변경을 묶고 무중단을 과도하게 단언한다.
| 기준 | ChatGPT | Gemini | 앞선 모델 |
|---|---|---|---|
| 지시 준수 | 9 | 8 | ChatGPT +13% |
| 정확성 | 8 | 6 | ChatGPT +33% |
| 구체성 | 9 | 9 | 동점 |
| 구성·가독성 | 9 | 8 | ChatGPT +13% |
| 분량 적정성 | 8 | 6 | ChatGPT +33% |
gpt-5.6-sol가 모델 이름을 가린 채 1~10점으로 채점했습니다 (2026-09-25). 측정값이 아니라 AI 심사 결과입니다.
같은 입력을 세 모델에 넣어 받은 답을 그대로 옮겼습니다. 각 CLI(에이전트 하네스)에서 실행했습니다. ChatGPT·Claude 앱이나 웹에서는 답이 다를 수 있습니다.
마이그레이션 SQL: ALTER TABLE orders ADD COLUMN refund_status VARCHAR(20) NOT NULL DEFAULT 'NONE'; ALTER TABLE orders ADD COLUMN refund_requested_at TIMESTAMP; CREATE INDEX idx_orders_refund_status ON orders (refund_status); ALTER TABLE orders ALTER COLUMN user_phone TYPE VARCHAR(20); 테이블 규모: orders 테이블 약 1,200만 행, 데이터 크기 18GB, 초당 30~40건 쓰기 발생 중 DB 종류·버전: PostgreSQL 14 (AWS RDS)
전제: PostgreSQL 14, RDS, orders에 지속적으로 읽기/쓰기 발생. 정확한 시간은 인스턴스의 스토리지 성능과 트랜잭션 대기 상태에 따라 달라지므로 아래 시간은 보수적인 운영 추정치입니다.
| 문장 | 락과 영향 | 테이블 크기 의존성 | 테이블 rewrite | 12M행 기준 예상 | 온라인 실행 |
|---|---|---|---|---|---|
ADD COLUMN refund_status VARCHAR(20) NOT NULL DEFAULT 'NONE' |
ACCESS EXCLUSIVE. 락을 보유하는 동안 읽기·쓰기 모두 차단 |
PostgreSQL 11+의 상수 기본값 fast path라 보통 상수 시간. 단, 락 대기는 무제한일 수 있음 | PostgreSQL 14에서는 rewrite하지 않음 | 보통 수 ms~수 초. 대기 중인 트랜잭션이 있으면 훨씬 길어짐 | 가능하지만 짧은 락을 보장해야 함. lock_timeout 권장 |
ADD COLUMN refund_requested_at TIMESTAMP |
ACCESS EXCLUSIVE. 짧게 읽기·쓰기 차단 |
상수 시간 | 하지 않음 | 보통 수 ms~수 초 | 가능 |
CREATE INDEX idx_orders_refund_status ... |
일반 CREATE INDEX는 SHARE 락. 읽기는 허용하지만 INSERT/UPDATE/DELETE 차단 |
테이블 스캔·인덱스 생성 시간이 행 수와 데이터 크기에 비례 | 테이블 자체는 rewrite하지 않음. 새 인덱스를 생성 | 18GB 기준 대략 수 분~수십 분. RDS 부하·스토리지 성능에 따라 더 길 수 있음 | 현재 문장 그대로는 비온라인. CREATE INDEX CONCURRENTLY 사용 |
ALTER COLUMN user_phone TYPE VARCHAR(20) |
ACCESS EXCLUSIVE. 읽기·쓰기 모두 차단 |
기존 타입과 데이터에 따라 다름. text/varchar 계열이라도 길이 검증 때문에 전체 스캔 가능 |
text/varchar 간 binary-coercible 변경은 보통 rewrite하지 않지만, 다른 타입이거나 USING 변환이 필요하면 rewrite 가능 |
전체 검증이 발생하면 수 분 이상. rewrite가 발생하면 18GB 기준 수 분~수십 분 이상 | 현재 문장 그대로는 위험. 사전 검증과 별도 배포 필요 |
ACCESS EXCLUSIVE 요청은 락을 즉시 얻지 못할 수 있습니다. 오래 열린 트랜잭션 하나가 있으면 DDL이 기다리고, 그 뒤에 들어온 쿼리까지 락 큐에서 지연될 수 있습니다. 따라서 “실행 자체는 빠른 DDL”도 운영 장애가 될 수 있습니다.
실행 전에는 다음을 확인하는 것이 좋습니다.
SELECT id, user_phone
FROM orders
WHERE user_phone IS NOT NULL
AND char_length(user_phone) > 20
LIMIT 100;
현재 타입도 확인합니다.
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'orders'
AND column_name = 'user_phone';
SET lock_timeout = '3s';
ALTER TABLE orders
ADD COLUMN refund_status varchar(20);
ALTER TABLE orders
ADD COLUMN refund_requested_at timestamp;
두 문장은 각각 별도 트랜잭션 또는 별도 실행으로 두는 편이 안전합니다.
애플리케이션은 다음을 먼저 배포합니다.
refund_status가 NULL인 기존 행을 일단 'NONE'으로 해석'NONE' 기록롤백:
ALTER TABLE orders DROP COLUMN refund_requested_at;
ALTER TABLE orders DROP COLUMN refund_status;
단, 애플리케이션이 해당 컬럼을 읽거나 쓴 뒤에는 먼저 애플리케이션을 이전 버전 호환 상태로 되돌려야 합니다. 컬럼 삭제는 데이터가 사라지는 작업이므로 별도 승인 후 실행해야 합니다.
한 번에 1,200만 행을 갱신하지 말고 기본키 범위 또는 ctid 기반으로 작은 배치로 처리합니다.
예:
UPDATE orders
SET refund_status = 'NONE'
WHERE id > :last_id
AND id <= :next_id
AND refund_status IS NULL;
배치 크기는 처음에는 1,000~10,000행 정도로 시작하고, 각 배치 사이에 짧은 간격을 둡니다. id가 없거나 적절한 인덱스가 없다면 별도 방식이 필요합니다.
롤백:
UPDATE orders
SET refund_status = NULL
WHERE ...;
하지만 애플리케이션이 이미 해당 값을 사용하거나 변경했다면 안전한 전체 롤백은 어렵습니다. 백필은 사실상 가역성이 제한되는 단계로, 실행 전에 명시해야 합니다.
백필이 끝난 뒤:
SET lock_timeout = '3s';
ALTER TABLE orders
ALTER COLUMN refund_status SET DEFAULT 'NONE';
이 작업은 짧은 ACCESS EXCLUSIVE 락만 필요합니다.
롤백:
ALTER TABLE orders
ALTER COLUMN refund_status DROP DEFAULT;
이것은 기존 행의 값을 되돌리지 않습니다.
운영 중 신규 쓰기가 계속되므로 먼저 새 쓰기에 대해 애플리케이션이 항상 값을 기록하는지 확인합니다.
그다음 안전한 방식은 CHECK 제약을 사용하는 것입니다.
ALTER TABLE orders
ADD CONSTRAINT orders_refund_status_not_null
CHECK (refund_status IS NOT NULL) NOT VALID;
NOT VALID 추가 자체는 짧은 락입니다. 이후 별도로 검증합니다.
ALTER TABLE orders
VALIDATE CONSTRAINT orders_refund_status_not_null;
검증은 기존 테이블을 스캔하므로 테이블 크기에 비례하지만, 일반적으로 읽기·쓰기를 허용하는 SHARE UPDATE EXCLUSIVE 락을 사용합니다. 다만 다른 DDL과 충돌할 수 있고, 실행 중 CPU·I/O를 사용합니다.
PostgreSQL 14에서 SET NOT NULL을 꼭 사용해야 한다면, 검증된 제약을 활용할 수 있는지 사전 확인 후 별도 실행합니다.
SET lock_timeout = '3s';
ALTER TABLE orders
ALTER COLUMN refund_status SET NOT NULL;
이 단계는 ACCESS EXCLUSIVE를 요구하므로 별도 배포로 두어야 합니다. 버전과 제약 형태에 따라 기존 검증을 재활용하지 못하고 다시 전체 스캔할 가능성이 있으므로, 운영에서는 검증된 CHECK 제약을 유지하는 방식도 고려할 수 있습니다.
롤백:
ALTER TABLE orders
ALTER COLUMN refund_status DROP NOT NULL;
ALTER TABLE orders
DROP CONSTRAINT orders_refund_status_not_null;
애플리케이션이 이미 NULL을 처리하지 않도록 변경됐다면 애플리케이션 롤백도 선행해야 합니다.
현재 문장 대신 다음을 사용합니다.
CREATE INDEX CONCURRENTLY idx_orders_refund_status
ON orders (refund_status);
CREATE INDEX CONCURRENTLY는 일반 인덱스 생성보다 오래 걸리고 테이블을 여러 번 스캔하지만, 일반적인 읽기·쓰기 작업을 차단하지 않습니다.
이 문장은 반드시 단독으로 실행해야 합니다.
INVALID 인덱스가 남을 수 있음실패 시:
DROP INDEX CONCURRENTLY IF EXISTS idx_orders_refund_status;
그 후 원인을 해결하고 다시 생성합니다. 인덱스 생성 중에는 디스크 여유 공간, I/O, CPU, replication lag를 확인해야 합니다.
user_phone 길이 제한먼저 애플리케이션이 20자를 넘는 값을 새로 쓰지 않도록 배포합니다. 기존 데이터 검증이 통과하면:
ALTER TABLE orders
ADD CONSTRAINT orders_user_phone_max_len
CHECK (user_phone IS NULL OR char_length(user_phone) <= 20)
NOT VALID;
별도 실행으로:
ALTER TABLE orders
VALIDATE CONSTRAINT orders_user_phone_max_len;
이 방식은 기존 컬럼 타입을 그대로 유지하면서 온라인으로 길이 제한을 검증할 수 있습니다. 반드시 타입 자체를 varchar(20)으로 바꿔야 한다면, 검증 완료 후 다음을 별도 실행합니다.
SET lock_timeout = '3s';
ALTER TABLE orders
ALTER COLUMN user_phone TYPE varchar(20);
그래도 ACCESS EXCLUSIVE 락이 필요하므로 트래픽이 낮고 오래 열린 트랜잭션이 없는 시점에 실행해야 합니다.
롤백:
ALTER TABLE orders
ALTER COLUMN user_phone TYPE <기존 타입>;
기존 타입이 무엇인지 확정해야 합니다. PostgreSQL의 타입 변경은 값을 자동으로 잘라내지 않으므로, 길이 초과 데이터가 있으면 실패합니다. 다만 애플리케이션이 이미 20자 제한에 의존하게 된 뒤에는 애플리케이션 롤백도 필요합니다.
다음은 다른 작업과 같은 트랜잭션에 넣지 않는 것이 좋습니다.
CREATE INDEX CONCURRENTLY ...
그리고 다음도 각각 별도 배포 또는 별도 트랜잭션으로 분리합니다.
ALTER COLUMN user_phone TYPE ...ALTER COLUMN refund_status SET NOT NULLVALIDATE CONSTRAINT특히 원래의 일반 CREATE INDEX는 쓰기를 차단하므로 사용하지 않는 것이 좋습니다.
확인할 항목:
pg_stat_activity: 대기 중인 DDL과 장기 트랜잭션pg_locks: ACCESS EXCLUSIVE 대기 여부중단 기준은 사전에 수치로 정해야 합니다. 예를 들면:
결론적으로 첫 두 개의 컬럼 추가는 PostgreSQL 14에서 비교적 안전하지만 락 대기를 관리해야 합니다. 가장 위험한 것은 일반 CREATE INDEX와 user_phone 타입 변경입니다. 인덱스는 CONCURRENTLY, 전화번호 길이는 CHECK NOT VALID → VALIDATE 경로로 처리하는 것이 안전합니다.
| 대용량 텍스트 변환기 프롬프트 | |
| AI 데이터 추출 도구 기획 프롬프트 | |
| AI2sql 자연어 SQL 생성 프롬프트 | |
| 백엔드 아키텍트 역할을 맡기는 프롬프트 | |
| Base R 프로그래밍 프롬프트 |