한 테이블에 여러 주제를 욱여넣어 같은 사실이 중복될 때 생기는 세 가지 문제. 정규화의 존재 이유다.
문제가 되는 테이블
| 주문ID | 회원ID | 회원등급 | 상품명 | 가격 |
|---|---|---|---|---|
| 1 | 42 | GOLD | 키보드 | 50000 |
| 2 | 42 | GOLD | 마우스 | 30000 |
| 3 | 99 | SILVER | 키보드 | 50000 |
회원 42의 등급(GOLD)과 키보드 가격(50000)이 여러 행에 중복돼 있다. 여기서 셋이 나온다.
세 가지
-
삽입 이상 — 주문하지 않은 신규 회원을 넣을 수 없다
- 상품명·가격 칸을 NULL 로 채운 가짜 행을 만들어야 한다
-
갱신 이상 — 회원 42가 PLATINUM 이 되면 행 1·2를 전부 고쳐야 한다
- 하나라도 빠뜨리면 같은 회원의 등급이 두 개가 된다 (모순)
-
삭제 이상 — 행 3을 지우면 '키보드 가격이 50000' 이라는 사실까지 사라진다
- 지우고 싶지 않은 정보가 딸려 나간다
실제로 어떻게 터지나
-- 갱신 이상이 만드는 데이터 모순
UPDATE orders SET member_grade = 'PLATINUM'
WHERE member_id = 42 AND order_id = 1; -- 행 1만 고쳤다
SELECT DISTINCT member_id, member_grade FROM orders WHERE member_id = 42;
-- 42 | PLATINUM
-- 42 | GOLD ← 한 회원에게 등급이 둘. 어느 게 진짜인가?
이 상태가 되면 어떤 값이 옳은지 DB가 알 수 없다. 등급별 할인을 계산하는 쿼리가 조회 방식에 따라 다른 답을 내기 시작한다.
해결 — 사실을 한 곳에만 둔다
-- 하나의 넓은 표를 주제별로 쪼갠다
CREATE TABLE member (member_id BIGINT PRIMARY KEY, grade VARCHAR(20));
CREATE TABLE product (name VARCHAR(100) PRIMARY KEY, price INT);
CREATE TABLE orders (order_id BIGINT PRIMARY KEY,
member_id BIGINT REFERENCES member(member_id),
product_name VARCHAR(100) REFERENCES product(name));
-- 이제 등급 변경은 한 행만 고치면 된다
UPDATE member SET grade = 'PLATINUM' WHERE member_id = 42;
각 사실이 정확히 한 곳에만 존재하므로 모순이 구조적으로 불가능해진다.
면접 함정
- ❌ "이상 현상은 데이터가 잘못 들어가서 생긴다" → 데이터가 아니라 스키마 설계가 원인이다. 아무리 조심해서 넣어도 구조가 그대로면 반복된다.
- ❌ "제약(FK·UNIQUE)을 걸면 해결된다" → 중복 자체를 못 막는다. 테이블을 쪼개야 한다.
이미 터진 모순을 찾아내기
-- 같은 회원인데 등급이 여러 개 → 갱신 이상이 이미 발생한 상태
SELECT member_id, count(DISTINCT member_grade) AS variants,
string_agg(DISTINCT member_grade, ', ') AS values
FROM orders GROUP BY member_id HAVING count(DISTINCT member_grade) > 1;
레거시 테이블을 정규화하기 전 실태 조사에 그대로 쓴다. 결과가 많으면 어느 값이 진짜인지 정하는 작업부터 해야 한다.
옮기는 절차
-- ① 새 테이블을 만들고 대표값을 뽑아 채운다 (가장 최근 주문의 등급을 채택)
INSERT INTO member (id, grade)
SELECT DISTINCT ON (member_id) member_id, member_grade
FROM orders ORDER BY member_id, created_at DESC;
-- ② 양쪽을 함께 쓰며 검증한다 (일정 기간)
-- ③ 읽기를 새 테이블로 전환한다
-- ④ 옛 칼럼을 제거한다
ALTER TABLE orders DROP COLUMN member_grade;
한 번에 바꾸지 않는다 — ②의 병행 기간이 사고를 막는다.