제약조건 설정 시 주의해야 할 부분
제약조건은 “거는 것”보다 “운영 중에 거는 것”이 어렵다
앞 글에서 제약조건의 종류를 정리했는데, 정작 실무에서 사람을 당황시키는 건 이미 데이터가 쌓인 테이블에 제약을 추가할 때 입니다. 직접 부딪혔던(혹은 부딪힐 뻔한) 함정들을 모아봤습니다. 😅
1. UNIQUE는 NULL을 중복으로 보지 않는다
가장 많이 헷갈리는 부분입니다. UNIQUE 제약이 걸린 컬럼에 NULL은 여러 개 들어갈 수 있습니다. SQL 표준상 NULL은 “값이 없음/알 수 없음”이라 서로 같다고 판단하지 않기 때문이에요.
1
2
3
CREATE TABLE t (code text UNIQUE);
INSERT INTO t VALUES (NULL);
INSERT INTO t VALUES (NULL); -- 에러 없이 통과!
“이메일이 유일해야 한다”고 UNIQUE만 걸어두면, NULL 이메일이 잔뜩 들어갈 수 있습니다. 진짜 하나만 허용하려면 UNIQUE + NOT NULL을 함께 걸어야 합니다.
PostgreSQL 15부터는
UNIQUE NULLS NOT DISTINCT옵션으로 NULL도 중복 취급하게 만들 수 있습니다.
2. 기존 테이블에 NOT NULL 추가 → 기존 NULL이 있으면 실패
데이터가 있는 테이블에 NOT NULL을 추가하려 했는데 이미 NULL 행이 있으면, ALTER TABLE은 그냥 실패합니다.
1
2
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
-- ERROR: column "phone" contains null values
순서를 지켜야 합니다. 먼저 기존 NULL을 채우고, 그다음 제약을 추가합니다.
1
2
UPDATE users SET phone = 'UNKNOWN' WHERE phone IS NULL;
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
3. CHECK에서 NULL은 통과한다
CHECK 조건은 결과가 TRUE면 통과, FALSE면 거부인데, NULL(unknown)은 거부하지 않습니다. 3-value 논리 때문이에요.
1
2
CREATE TABLE products (price integer CHECK (price >= 0));
INSERT INTO products VALUES (NULL); -- CHECK 통과! (NULL은 거부 안 됨)
가격이 음수가 아니어야 한다고 CHECK (price >= 0)만 걸면 NULL은 막지 못합니다. 필요하면 NOT NULL을 같이 걸어야 합니다.
4. 큰 테이블에 제약 추가는 잠금(LOCK)을 유발한다
운영 중인 대형 테이블에 제약을 그냥 추가하면, 검증을 위해 테이블 전체를 훑으면서 그동안 쓰기 잠금이 걸릴 수 있습니다. 트래픽이 있는 시간대엔 서비스가 멈춰 보일 수 있어요.
PostgreSQL은 이걸 두 단계로 나눌 수 있게 해줍니다. 먼저 NOT VALID로 제약을 “앞으로 들어올 데이터에만” 적용하고(기존 데이터 전체 스캔 생략), 나중에 한가할 때 VALIDATE로 기존 데이터를 검증합니다.
1
2
3
4
5
6
7
-- 1) 기존 데이터는 검증하지 않고 빠르게 추가
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users (id) NOT VALID;
-- 2) 트래픽 적은 시간에 기존 데이터 검증 (약한 잠금)
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user;
5. 외래 키 컬럼엔 인덱스를 직접 걸어주자
FOREIGN KEY를 만들어도, 참조하는 쪽(자식) 컬럼에는 인덱스가 자동으로 생기지 않습니다. 부모 행을 DELETE/UPDATE할 때 자식 테이블을 확인해야 하는데, 인덱스가 없으면 자식 전체를 스캔해서 느려집니다.
1
2
-- FK 컬럼에는 보통 직접 인덱스를 걸어준다
CREATE INDEX idx_orders_user_id ON orders (user_id);
정리 & 체크리스트
UNIQUE는NULL중복을 허용한다 → 진짜 유일하게 하려면NOT NULL병행.NOT NULL추가 전, 기존NULL데이터부터 처리.CHECK는NULL을 통과시킨다.- 대형 테이블엔
NOT VALID→VALIDATE2단계로 잠금 최소화. - FK 컬럼엔 인덱스를 직접 걸어 삭제/수정 성능을 챙기자.
