포스트

제약조건 설정 시 주의해야 할 부분

제약조건 설정 시 주의해야 할 부분

제약조건은 “거는 것”보다 “운영 중에 거는 것”이 어렵다

앞 글에서 제약조건의 종류를 정리했는데, 정작 실무에서 사람을 당황시키는 건 이미 데이터가 쌓인 테이블에 제약을 추가할 때 입니다. 직접 부딪혔던(혹은 부딪힐 뻔한) 함정들을 모아봤습니다. 😅

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 → VALIDATE 2단계로 잠금 최소화.
  • FK 컬럼엔 인덱스를 직접 걸어 삭제/수정 성능을 챙기자.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.