Quick Reference
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL
);외래 키는 존재하지 않는 부모를 참조하지 못하게 하고, ON DELETE와 ON UPDATE는 부모 키가 바뀌거나 사라질 때 자식을 정합니다. 함께 사라져야만 하는 종속 데이터만 CASCADE를 쓰고, 보존해야 하는 기록은 기본값인 NO ACTION 또는 명시적 RESTRICT로 삭제를 막는 쪽부터 검토합니다. 자식 FK 인덱스는 자동 생성되지 않으므로, 부모 삭제·갱신이 잦고 자식 테이블이 크면 별도로 둡니다.
문법
외래 키는 관계의 존재 자체를 DB가 보장하게 만든다
외래 키 제약 없이 user_id를 저장하면, 존재하지 않는 user_id가 들어와도 DB는 이를 막지 않습니다. 외래 키를 선언하면 INSERT·UPDATE 시 참조 대상이 실제로 존재하는지 확인하고, 없으면 오류를 냅니다. 이 검사를 애플리케이션 코드에서만 수행하면 마이그레이션, 배치 작업, 직접 SQL 등 다른 경로로 데이터가 들어올 때 무결성이 깨질 수 있습니다. DB 레벨 제약은 어떤 경로로 데이터가 들어와도 일관성을 지켜줍니다.
-- 외래 키 위반 예시
INSERT INTO posts (user_id, title) VALUES (9999, '존재하지 않는 사용자');
-- ERROR: insert or update on table "posts" violates foreign key constraint
-- DETAIL: Key (user_id)=(9999) is not present in table "users".외래 키와 삭제 규칙은 같이 설계해야 한다
외래 키는 "부모가 실제로 존재해야 한다"를 보장하고, ON DELETE 규칙은 "부모가 사라질 때 자식을 어떻게 할지"를 정합니다. 둘은 따로가 아니라 한 관계의 두 부분이라고 보면 됩니다.
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
body TEXT NOT NULL
);ON DELETE 규칙은 부모 삭제 시 자식의 운명을 결정한다
부모 행이 삭제될 때 자식 행을 어떻게 처리할지는 다섯 선택지로 나뉩니다. 같은 규칙을 ON UPDATE에도 둘 수 있습니다. NO ACTION과 RESTRICT는 일반적인 즉시 검사에서는 비슷해 보이지만, 제약을 지연할 수 있는지에서 차이가 납니다.
| 규칙 | 동작과 제약 |
|---|---|
NO ACTION (기본) | 제약을 검사하는 시점에 자식이 있으면 거부. DEFERRABLE 제약에서는 트랜잭션 끝까지 검사를 미룰 수 있음 |
RESTRICT | 참조 변경·삭제를 즉시 막음. DEFERRABLE로 만들 수 없음 |
CASCADE | 부모 삭제 시 자식도 함께 삭제 |
SET NULL | 부모 삭제 시 자식 FK를 NULL로 변경. 자식 컬럼이 nullable이어야 함 |
SET DEFAULT | 자식 FK를 기본값으로 변경. 그 기본값도 유효한 부모를 참조해야 함 |
-- 사용자 삭제 시 게시글도 함께 삭제 (CASCADE)
CREATE TABLE posts (
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE
);
-- 작성자 삭제 시 게시글의 author_id를 NULL로 (SET NULL)
CREATE TABLE posts (
author_id BIGINT REFERENCES users(id) ON DELETE SET NULL
);CASCADE는 연쇄 삭제 범위를 먼저 파악해야 안전하다
ON DELETE CASCADE는 편리하지만 연쇄 범위가 생각보다 클 수 있습니다. users → posts → comments 가 모두 CASCADE라면, 사용자 한 명을 삭제할 때 그 사람의 모든 글과 모든 댓글이 즉시 삭제됩니다. 이 삭제는 롤백하지 않는 한 복구가 어렵습니다. 삭제 범위가 큰 데이터(결제 기록, 주문 이력 등)에는 CASCADE 대신 RESTRICT로 삭제를 막거나, 소프트 딜리트(deleted_at 컬럼)를 사용하는 편이 안전합니다.
-- 소프트 딜리트: CASCADE 대신 deleted_at으로 상태 관리
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ;
UPDATE users SET deleted_at = NOW() WHERE id = 42;
-- posts는 그대로 유지되고, 애플리케이션에서 deleted_at IS NULL 조건으로 필터링외래 키 컬럼에 인덱스를 따로 걸어야 하는 이유
PostgreSQL은 외래 키 제약을 선언해도 자식 테이블의 외래 키 컬럼에 자동으로 인덱스를 만들지 않습니다. 부모 행을 삭제하거나 키를 갱신할 때 PostgreSQL은 자식 테이블의 참조 행을 확인합니다. 자식이 크고 이런 변경이 일어나면 적절한 선두 컬럼 인덱스가 없을 때 비용이 커질 수 있습니다. 반대로 부모를 거의 삭제·갱신하지 않고 자식도 작다면, 모든 FK에 기계적으로 인덱스를 더할 이유는 없습니다. 조인·조회 조건과 부모 변경 경로를 함께 보고 만듭니다.
-- 외래 키 컬럼에 인덱스 추가
CREATE INDEX idx_posts_user_id ON posts (user_id);
CREATE INDEX idx_comments_post_id ON comments (post_id);
-- 인덱스 없는 경우 부모 삭제·키 변경 시 Seq Scan이 발생할 수 있다.
-- EXPLAIN DELETE FROM users WHERE id = 42;
-- → Seq Scan on posts (checking for referencing rows)순환 참조나 한 작업 안의 순서 교환에는 지연 제약을 쓴다
두 테이블이 서로를 참조하거나, 한 트랜잭션 안에서 임시로 참조 순서가 어긋나는 작업은 DEFERRABLE 외래 키가 필요할 수 있습니다. 기본 FK는 즉시 검사하므로 아래처럼 명시해야 합니다.
ALTER TABLE employees
ADD CONSTRAINT employees_manager_fk
FOREIGN KEY (manager_id) REFERENCES employees(id)
DEFERRABLE INITIALLY DEFERRED;
BEGIN;
INSERT INTO employees (id, manager_id) VALUES (1, 2);
INSERT INTO employees (id, manager_id) VALUES (2, 1);
COMMIT; -- 여기서 두 참조가 모두 유효한지 검사지연 제약은 RESTRICT와 함께 쓸 수 없고, 오류가 INSERT 문장이 아니라 COMMIT에서 날 수 있습니다. 일반적인 단방향 관계에는 기본 즉시 검사가 원인을 더 빨리 드러내므로 먼저 선택할 이유가 충분합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 부모 삭제 시 자식도 함께 없애도 될 때 | ON DELETE CASCADE |
| 부모가 삭제되면 자식의 참조를 지워도 될 때 | ON DELETE SET NULL |
| 자식이 있는 부모는 삭제 자체를 막으려면 | 기본 NO ACTION 또는 즉시 차단이 필요한 RESTRICT |
| 삭제 범위가 크거나 복구가 필요한 데이터 | 소프트 딜리트 패턴 (deleted_at) |
주의할 점
CASCADE는 연쇄 삭제 범위가 예상보다 넓을 수 있습니다. 부모 한 행 삭제가 수천 건의 자식 데이터를
즉시 제거할 수 있으며, 이는 롤백하지 않으면 복구가 어렵습니다. 또한 PostgreSQL은 외래 키 컬럼에
인덱스를 자동 생성하지 않습니다. 자식이 크고 부모 삭제·키 변경이 일어나는 관계는 실행 계획을 확인해
적절한 자식 인덱스를 추가하세요.
DELETE FROM users WHERE id = 42;기본 NO ACTION 관계에서 자식 행이 남아 있으면, 위 삭제는 제약 검사 시 실패합니다. "왜 삭제가 안 되지?"가 아니라 "지금 관계를 지우지 말라는 규칙이 작동 중"이라고 보면 됩니다.
참고 링크
2 sources