외래 키와 참조 무결성 — FOREIGN KEY의 작동 원리
FOREIGN KEY가 참조 무결성을 어떻게 강제하는지, ON DELETE/ON UPDATE 다섯 가지 옵션의 차이, DEFERRABLE 지연 검사, 그리고 실전에서 FK를 끄는 상황까지 다룹니다.
지난 글에서 기본 키 설계를 다뤘다. 이번에는 테이블 간의 관계를 정의하고 참조 무결성을 보장하는 FOREIGN KEY를 살펴본다.
참조 무결성이란
참조 무결성(Referential Integrity) 은 자식 테이블의 외래 키 값이 항상 부모 테이블의 기본 키(또는 UNIQUE) 값과 일치해야 한다는 규칙이다. 예를 들어 orders.customer_id가 존재하지 않는 고객을 가리킬 수 없다.
FOREIGN KEY 제약이 없으면 참조 무결성은 애플리케이션 코드에만 의존하게 되고, 직접 SQL이나 배치 작업이 이 규칙을 우회하는 순간 데이터는 조용히 망가진다.
FOREIGN KEY 선언
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
total NUMERIC(12,2) NOT NULL,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id)
REFERENCES customers (customer_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
REFERENCES 뒤에 오는 컬럼은 부모 테이블의 PRIMARY KEY 또는 UNIQUE 제약이 걸린 컬럼이어야 한다.
ON DELETE / ON UPDATE 옵션 상세
부모 행이 삭제되거나 PK 값이 변경될 때 자식 행을 어떻게 처리할지를 정의한다.
RESTRICT (기본값)
자식 행이 존재하면 부모 행의 삭제·수정을 즉시 거부한다. 가장 안전한 옵션으로, 명시하지 않으면 대부분의 DBMS에서 기본값이다.
-- customer_id=1인 주문이 있으면 아래 DELETE는 실패
DELETE FROM customers WHERE customer_id = 1;
-- ERROR: violates foreign key constraint
CASCADE
부모 행이 삭제되면 자식 행도 함께 삭제된다. 주문-주문상세처럼 부모 없이는 의미 없는 자식에 적합하다.
ON DELETE CASCADE -- 고객 삭제 시 주문도 모두 삭제
ON UPDATE CASCADE -- 고객 ID 변경 시 주문의 FK도 자동 업데이트
CASCADE는 편리하지만 예상치 못한 대량 삭제를 유발할 수 있다. 중요한 비즈니스 데이터에는 신중히 사용한다.
SET NULL
부모 행 삭제 시 자식의 FK 컬럼을 NULL로 바꾼다. 담당자가 퇴사해도 고객 레코드는 남겨야 하는 경우처럼, 선택적 관계에서 유용하다.
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
name VARCHAR(100),
sales_rep_id BIGINT, -- NULL 허용 필수
CONSTRAINT fk_sales_rep
FOREIGN KEY (sales_rep_id)
REFERENCES employees (emp_id)
ON DELETE SET NULL
);
SET NULL을 사용하려면 해당 FK 컬럼이 NULL을 허용해야 한다.
NO ACTION
RESTRICT와 비슷하지만, 트랜잭션이 끝날 때까지 검사를 미룬다. DEFERRABLE과 함께 사용할 때 의미가 있다.
DEFERRABLE — 지연 검사
순환 참조나 배치 데이터 로딩처럼 “일시적으로 무결성이 깨져도 커밋 전에 복구되는” 시나리오에서 유용하다. PostgreSQL이 지원하며, MySQL은 지원하지 않는다.
-- 선언 시
CONSTRAINT fk_parent
FOREIGN KEY (parent_id) REFERENCES parent(id)
DEFERRABLE INITIALLY DEFERRED
-- 또는 세션에서 임시 활성화
SET CONSTRAINTS fk_parent DEFERRED;
-- 이 시점에는 FK 위반 허용
INSERT INTO child (parent_id) VALUES (999);
INSERT INTO parent (id) VALUES (999); -- 커밋 전에 복구
COMMIT; -- 커밋 시점에 검사
FK와 성능 — 인덱스 필수
FK 컬럼에는 반드시 인덱스를 생성해야 한다. 부모 행 삭제 시 DB가 자식 테이블을 풀 스캔하기 때문이다.
-- FK 컬럼에 인덱스 추가 (MySQL은 자동 생성, PostgreSQL은 수동)
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
PostgreSQL은 FK 생성 시 자식 쪽 인덱스를 자동으로 만들지 않는다. FK를 걸 때마다 인덱스도 함께 생성하는 습관이 중요하다.
실전: FK를 끄는 경우
FK가 데이터 무결성을 보장하는 좋은 수단이지만, 대량 데이터 로딩(ETL, 마이그레이션)이나 특정 NoSQL 스타일 설계에서는 의도적으로 비활성화하기도 한다.
-- MySQL: FK 검사 임시 비활성화
SET FOREIGN_KEY_CHECKS = 0;
-- 대량 INSERT ...
SET FOREIGN_KEY_CHECKS = 1;
-- PostgreSQL: 테이블의 모든 트리거(FK 포함) 임시 비활성화
ALTER TABLE orders DISABLE TRIGGER ALL;
-- 로딩 후
ALTER TABLE orders ENABLE TRIGGER ALL;
FK를 끄는 작업은 반드시 범위를 최소화하고, 끈 후에 데이터 일관성을 직접 검증해야 한다.
다음 글에서는 UNIQUE 제약이 NULL을 어떻게 다루는지, 그리고 기본 키와 어떻게 다른지를 살펴본다.
지난 글: 기본 키 설계 — PRIMARY KEY의 본질과 전략
다음 글: 유니크 제약 — UNIQUE 인덱스와 NULL 허용 동작
읽어주셔서 감사합니다. 😊