설치·접속: PostgreSQL 설치와 접속
부제: 사용자를 탈퇴시킬 때 리뷰는 익명으로 남기고, 장바구니는 같이 지우고, 주문 이력은 못 지우게 막고 싶을 때
한 사용자를 지울 때 딸린 데이터를 참조 종류마다 다르게 처리하고 싶다. 리뷰는 작성자만 NULL로(익명화), 장바구니는 함께 삭제, 주문은 삭제 자체를 막는다. FK의 ON DELETE 액션으로 세 정책을 각각 선언한다.
CREATE TABLE app_user (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text
);
-- 리뷰: 작성자 삭제 시 author_id만 NULL (내용은 남김)
CREATE TABLE review (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author_id integer REFERENCES app_user(id) ON DELETE SET NULL,
body text
);
-- 장바구니: 사용자 삭제 시 함께 제거
CREATE TABLE cart (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id integer REFERENCES app_user(id) ON DELETE CASCADE,
item text
);
-- 주문: 남아 있으면 사용자 삭제를 막음
CREATE TABLE sales_order (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id integer REFERENCES app_user(id) ON DELETE RESTRICT,
amount numeric
);
INSERT INTO app_user (name) VALUES ('kim');
INSERT INTO review (author_id, body) VALUES (1, 'great product');
INSERT INTO cart (user_id, item) VALUES (1, 'keyboard');
INSERT INTO sales_order (user_id, amount) VALUES (1, 29900);
-- 주문이 남아 있는 상태에서 사용자 삭제 시도
DELETE FROM app_user WHERE id=1;
ERROR: update or delete on table "app_user" violates foreign key constraint "sales_order_user_id_fkey" on table "sales_order"
DETAIL: Key (id)=(1) is still referenced from table "sales_order".RESTRICT 덕분에 주문 이력이 남아 있는 한 사용자 삭제가 막힌다. 정산·회계상 지워선 안 되는 참조를 지키는 안전장치다. 이제 주문을 먼저 정리(또는 다른 사용자로 이관)한 뒤 삭제하면 나머지 두 정책이 작동한다.
DELETE FROM sales_order WHERE user_id=1;
DELETE FROM app_user WHERE id=1;
SELECT 'review (SET NULL)' AS what, id, author_id::text, body FROM review
UNION ALL
SELECT 'cart (CASCADE) count', NULL, count(*)::text, NULL FROM cart;
what | id | author_id | body
----------------------+----+-----------+---------------
review (SET NULL) | 1 | | great product
cart (CASCADE) count | | 0 |리뷰는 author_id가 NULL로 바뀌어 내용은 남고 작성자만 익명화됐고, 장바구니 행은 사용자와 함께 사라졌다. SET NULL은 참조 컬럼만 비우고(PG16은 ON DELETE SET NULL (col)로 특정 컬럼 부분집합 지정도 가능), CASCADE는 자식 행까지 연쇄 삭제한다. CASCADE는 편하지만 대량 연쇄 삭제 사고의 원인이 되니 어디까지 번지는지 항상 따져야 한다.
RESTRICT와 기본값 NO ACTION은 둘 다 "참조가 남아 있으면 막는다"로 결과가 같아 보이지만, 검사 시점이 다르다. RESTRICT는 즉시(그 DELETE 문에서) 검사해 지연이 안 되고, NO ACTION은 지연 가능해서 같은 트랜잭션 안에서 위반을 잠깐 만들었다가 커밋 전에 바로잡을 수 있다.
-- NO ACTION + DEFERRED: 부모를 지웠다가 자식을 다른 부모로 옮기고 커밋
CREATE TABLE parent (id integer PRIMARY KEY);
CREATE TABLE child (
id integer PRIMARY KEY,
pid integer REFERENCES parent(id) ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
);
INSERT INTO parent VALUES (1);
INSERT INTO child VALUES (10, 1);
BEGIN;
DELETE FROM parent WHERE id=1; -- 잠깐 고아 상태(NO ACTION이라 지연 검사)
INSERT INTO parent VALUES (2);
UPDATE child SET pid=2 WHERE id=10; -- 커밋 전에 다른 부모로 재연결
COMMIT;
SELECT * FROM child;
BEGIN
DELETE 1
INSERT 0 1
UPDATE 1
COMMIT
id | pid
----+-----
10 | 2RESTRICT였다면 첫 DELETE FROM parent에서 바로 에러가 났을 것이다. NO ACTION + DEFERRABLE이라 중간의 고아 상태가 허용되고 커밋 시점에만 정합성을 본다. "삭제를 무조건 막을 것인가(RESTRICT)", "재배치 여지를 줄 것인가(NO ACTION)"로 골라 쓴다.
이렇게도 쓴다
부모의 PK가 바뀔 때 자식도 따라 바꾼다. (ON UPDATE)
user_id integer REFERENCES app_user(id) ON UPDATE CASCADE ON DELETE RESTRICT
SET DEFAULT로 삭제 시 기본 부모(예: '탈퇴회원' 계정)로 귀속시킨다.
author_id integer DEFAULT 0 REFERENCES app_user(id) ON DELETE SET DEFAULT
PG16의 컬럼 부분집합 SET NULL — 복합 FK에서 일부 컬럼만 NULL로.
FOREIGN KEY (tenant_id, user_id) REFERENCES member(tenant_id, id)
ON DELETE SET NULL (user_id)
연쇄 삭제 범위가 걱정되면 CASCADE 대신 soft delete 트리거로 갈아탄다(다른 도구로 갈아탈 경계).
-- 실제 DELETE 대신 deleted_at 갱신 → 자식은 애플리케이션/뷰에서 필터
UPDATE app_user SET deleted_at = now() WHERE id = 1;'명령어 > DB' 카테고리의 다른 글
| [PostgreSQL] pg_stat_statements — 어떤 쿼리가 DB를 잡아먹는지 총시간 순으로 찾는다 (0) | 2026.08.02 |
|---|---|
| [PostgreSQL] pg_hba.conf로 "누가·어디서·어떻게" 붙는지 규칙을 읽는다 (0) | 2026.08.01 |
| [PostgreSQL] normal_rand로 정규분포 테스트 데이터를 뽑는다 (0) | 2026.08.01 |
| [PostgreSQL] 쿼리가 멈춰 있을 때 누가 누구를 막고 있나 — pg_locks와 pg_blocking_pids (0) | 2026.08.01 |
| [PostgreSQL] LATERAL JOIN 사용자마다 최근 주문 N건씩 딱 붙여 뽑는다 (0) | 2026.08.01 |