3NF: 이행적 함수 종속을 제거해 갱신 이상을 막는 정규형
제3정규형(3NF)은 2NF를 만족하면서 기본키가 아닌 속성이 다른 비키 속성에 종속되는 이행적 함수 종속(transitive dependency)을 제거한 릴레이션 상태다. 이를 통해 삽입·갱신·삭제 이상(anomaly)과 데이터 중복을 줄인다. PostgreSQL 예제로 3NF 위반 테이블을 분해하고 JOIN으로 복원하는 과정을 다룬다.
By 송민성
1. 개념
제3정규형(Third Normal Form, 3NF)은 릴레이션이 다음 조건을 만족하는 상태다.
- 제2정규형(2NF)을 만족한다.
- 모든 비키 속성(non-prime attribute)이 기본키(후보키)에 이행적으로 종속(transitive dependency)되지 않는다.
이행적 종속이란 A → B, B → C이고 B가 키가 아닐 때 A → C가 성립하는 관계다. 예를 들어 주문번호 → 고객ID, 고객ID → 고객등급이면 주문번호 → 고객등급은 고객ID를 거친 이행적 종속이다.
같은 의미의 정의로, 함수 종속 X → A가 있을 때 다음 중 하나가 항상 성립해야 3NF다.
- X가 슈퍼키(superkey)이다.
- A가 어떤 후보키(candidate key)에 포함된 속성(prime attribute)이다.
2. 왜 사용하는가
이행적 종속이 남아 있으면 같은 사실이 여러 행에 반복 저장되고, 그 결과 이상(anomaly)이 생긴다.
- 갱신 이상(update anomaly): 고객 등급을 바꾸려면 그 고객의 모든 주문 행을 고쳐야 한다. 일부만 고치면 모순된 데이터가 남는다.
- 삽입 이상(insertion anomaly): 주문이 없는 고객의 등급 정보를 저장할 곳이 없다.
- 삭제 이상(deletion anomaly): 고객의 마지막 주문을 지우면 고객 등급 정보도 함께 사라진다.
3NF로 분해하면 "고객의 등급"이라는 사실이 한 곳에만 저장되어 일관성을 유지하기 쉽다.
3. 동작 원리
정규화는 함수 종속(functional dependency)을 분석해 릴레이션을 분해하는 과정이다.
1단계: 함수 종속 나열
order_id → customer_id, order_date
customer_id → customer_grade, customer_name기본키는 order_id이고, customer_grade와 customer_name은 customer_id를 거쳐 이행적으로 종속된다.
2단계: 이행적 종속을 일으키는 결정자를 기준으로 분해
orders(order_id, customer_id, order_date):customer_id는 외래키(FK)customers(customer_id, customer_name, customer_grade)
3단계: 무손실 분해(lossless-join) 확인
분해한 두 릴레이션을 customer_id로 자연 조인(natural join)하면 원래 릴레이션이 정확히 복원되어야 한다. 공통 속성 customer_id가 customers의 키이므로 이 조건이 보장된다.
참고로 3NF는 BCNF(Boyce-Codd Normal Form)보다 약한 조건이다. 위 정의의 2번 조건(A가 prime attribute) 때문에 후보키가 여러 개 겹치는 특수한 경우에 3NF는 만족하지만 BCNF는 위반하는 릴레이션이 존재한다. 또한 어떤 릴레이션이든 무손실 분해와 함수 종속 보존을 모두 만족하는 3NF 분해가 항상 가능하다. BCNF는 종속 보존을 보장하지 못할 수 있다.
4. 코드 예제
PostgreSQL 기준으로 3NF 위반 테이블과 분해 결과를 비교한다.
-- 3NF 위반: customer_grade가 customer_id를 거쳐 order_id에 이행적으로 종속
CREATE TABLE orders_unnormalized (
order_id integer PRIMARY KEY,
customer_id integer NOT NULL,
customer_name text NOT NULL,
customer_grade text NOT NULL,
order_date date NOT NULL
);
INSERT INTO orders_unnormalized VALUES
(1, 10, 'Kim', 'GOLD', '2026-01-05'),
(2, 10, 'Kim', 'GOLD', '2026-02-11'),
(3, 20, 'Lee', 'SILVER', '2026-02-12');
-- 갱신 이상: 일부 행만 바뀌면 같은 고객의 등급이 서로 달라진다
UPDATE orders_unnormalized
SET customer_grade = 'VIP'
WHERE order_id = 1;
SELECT DISTINCT customer_id, customer_grade
FROM orders_unnormalized
WHERE customer_id = 10; -- GOLD, VIP 두 행이 나온다
-- 3NF 분해
CREATE TABLE customers (
customer_id integer PRIMARY KEY,
customer_name text NOT NULL,
customer_grade text NOT NULL
);
CREATE TABLE orders (
order_id integer PRIMARY KEY,
customer_id integer NOT NULL REFERENCES customers (customer_id),
order_date date NOT NULL
);
INSERT INTO customers VALUES
(10, 'Kim', 'GOLD'),
(20, 'Lee', 'SILVER');
INSERT INTO orders VALUES
(1, 10, '2026-01-05'),
(2, 10, '2026-02-11'),
(3, 20, '2026-02-12');
-- 등급 변경은 한 행만 수정하면 된다
UPDATE customers SET customer_grade = 'VIP' WHERE customer_id = 10;
-- 원래 형태가 필요하면 JOIN으로 복원한다
SELECT o.order_id, o.customer_id, c.customer_name, c.customer_grade, o.order_date
FROM orders o
JOIN customers c USING (customer_id)
ORDER BY o.order_id;5. 시간 복잡도 또는 성능 특성
3NF 분해는 쓰기와 조회에 서로 다른 영향을 준다.
- 쓰기: 중복 저장이 줄어 저장 공간이 감소하고, 한 사실을 바꾸는 갱신이 한 행에서 끝난다.
- 조회: 여러 테이블의 데이터가 필요하면 JOIN이 필요하다. 외래키 컬럼(
orders.customer_id)에 인덱스가 없으면 조인 비용이 커질 수 있다. PostgreSQL은 기본키에는 자동으로 인덱스를 만들지만 외래키 컬럼에는 만들지 않는다. - JOIN 비용은 데이터 크기, 인덱스, 플래너가 고르는 조인 방식(nested loop, hash join, merge join)에 따라 달라지므로 일률적인 복잡도로 말할 수 없다. 실제 비용은
EXPLAIN ANALYZE로 확인한다.
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
EXPLAIN ANALYZE
SELECT o.order_id, c.customer_grade
FROM orders o
JOIN customers c USING (customer_id)
WHERE o.customer_id = 10;6. 실무 사용 사례
- OLTP 시스템: 주문, 결제, 회원 관리처럼 쓰기가 잦고 정합성이 중요한 시스템은 기본적으로 3NF 수준으로 설계한다.
- 마스터 데이터 분리: 고객 등급, 상품 카테고리, 우편번호-지역처럼 "어떤 키가 정해지면 함께 정해지는 값"은 별도 테이블로 분리한다.
- 의도적 반정규화(denormalization): 조회 성능이 병목일 때 읽기 전용 리포트 테이블, 머티리얼라이즈드 뷰(materialized view), 집계 컬럼으로 중복을 허용한다. 이때도 원본은 3NF로 두고 파생 데이터로 관리하는 방식이 안전하다.
7. 주의할 점
- 시점에 따라 달라지는 값: 주문 당시의 등급이나 가격은 고객 테이블을 참조하면 과거 기록이 바뀐다. 이런 값은 이행적 종속이 아니라 "주문 시점의 사실"이므로 주문 행에 스냅샷으로 저장하는 것이 올바른 설계다.
- 함수 종속은 데이터가 아닌 의미로 판단한다: 현재 데이터에서 우연히 값이 일치하는 것만으로는 종속이 성립하지 않는다. 업무 규칙에서 도출해야 한다.
- 3NF가 모든 이상을 없애지는 않는다: 후보키가 겹치는 경우 BCNF 위반이 남을 수 있고, 다치 종속(multivalued dependency)은 4NF에서 다룬다.
- 외래키 인덱스 누락: 분해 후 조인 성능이 떨어지는 가장 흔한 원인이다.
- 과도한 분해: 항상 같이 조회되고 거의 변하지 않는 값까지 쪼개면 복잡도만 늘 수 있다. 정합성 요구와 조회 패턴을 함께 따져 판단한다.
8. 핵심 정리
- 3NF = 2NF + 비키 속성 간의 이행적 함수 종속 제거.
- 모든 함수 종속
X → A에서 X가 슈퍼키이거나 A가 prime attribute여야 한다. - 분해는 무손실 조인이 되도록 결정자(
customer_id)를 기준으로 한다. - 효과: 중복 감소, 갱신·삽입·삭제 이상 방지.
- 비용: 조회 시 JOIN이 늘어나므로 외래키 인덱스와
EXPLAIN ANALYZE로 확인한다. - 성능 목적의 반정규화는 3NF 설계를 기준으로 삼아 의도적으로 적용한다.