테이블이 어떤 나쁜 종속까지 제거됐는지를 나타내는 단계. 위로 갈수록 중복이 줄고 조인이 는다.
단계별로 무엇을 제거하나
비정규형 반복 그룹 · 다중값
↓ 값을 원자로
1NF 한 칸에 값 하나
↓ 부분 종속 제거
2NF 비키 속성이 복합키 '전체' 에 의존
↓ 이행 종속 제거
3NF 비키 속성이 키에 '직접' 의존
↓ 모든 결정자를 후보키로
BCNF 3NF 의 빈틈까지 메움
1NF — 칸 하나에 값 하나
-- ❌ 위반: 한 칸에 여러 값 / 반복 그룹
CREATE TABLE member (id BIGINT, hobbies VARCHAR(200)); -- '독서,등산,요리'
CREATE TABLE member (id BIGINT, phone1 VARCHAR(20), phone2 VARCHAR(20), phone3 VARCHAR(20));
-- ✅ 값을 행으로 펼친다
CREATE TABLE member_hobby (member_id BIGINT, hobby VARCHAR(50),
PRIMARY KEY (member_id, hobby));
PostgreSQL은 배열·JSONB를 지원해 "한 칸에 여러 값"이 물리적으로 가능하지만, 조인·제약의 대상이 되는 값은 별도 행으로 두는 게 정석이다. 배열은 편의이지 1NF의 면제가 아니다.
2NF — 복합키일 때만 문제된다
-- ❌ 주문일은 주문ID 만으로 정해진다 (부분 종속)
CREATE TABLE order_item (order_id BIGINT, product_id BIGINT,
order_date DATE, product_name VARCHAR(100), qty INT,
PRIMARY KEY (order_id, product_id));
-- ✅ 각자의 주제로 분리
CREATE TABLE orders (order_id BIGINT PRIMARY KEY, order_date DATE);
CREATE TABLE product (product_id BIGINT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE order_item (order_id BIGINT, product_id BIGINT, qty INT,
PRIMARY KEY (order_id, product_id));
단일 대리키면 부분 종속이 애초에 불가능하므로 2NF는 자동 충족된다.
3NF — 실무의 목표 지점
-- ❌ 등급명이 등급코드를 거쳐 의존한다 (이행 종속)
CREATE TABLE member (id BIGINT PRIMARY KEY, grade_code CHAR(2), grade_name VARCHAR(20));
-- ✅ 등급을 별도 테이블로
CREATE TABLE grade (code CHAR(2) PRIMARY KEY, name VARCHAR(20));
CREATE TABLE member (id BIGINT PRIMARY KEY, grade_code CHAR(2) REFERENCES grade(code));
외우는 문장이 있다 — "the key, the whole key, and nothing but the key" (키에, 키 전체에, 오직 키에만 의존).
4NF·5NF는 왜 잘 안 나오나
- 4NF — 독립적인 다중값 둘을 한 표에 두지 않는다
- (한 사람의 '취미' 목록과 '자격증' 목록을 같은 표에 → 불필요한 곱이 생긴다)
- 5NF — 세 개 이상으로 쪼개야만 무손실이 되는 매우 드문 경우
의식적으로 4NF·5NF까지 가는 일은 드물지만, "독립적인 다중값은 한 표에 같이 두지 않는다" 는 직관은 그대로 쓸모 있다.
면접 함정
- ❌ "정규화는 높을수록 좋다" → 조인이 늘어 읽기가 느려진다. 실무 기본값은 3NF이고, 그 위는 필요할 때만 간다.
- ❌ "1NF는 당연히 지켜진다" → 콤마로 이어 붙인 문자열,
col1/col2/col3반복 칼럼은 지금도 흔하다.