1NF: 모든 컬럼을 원자값으로 만들어 한 행·한 칸·한 값을 보장하는 제1정규형
제1정규형(1NF)은 관계형 테이블의 모든 컬럼이 더 쪼갤 수 없는 원자값(atomic value)만 갖고, 반복 그룹(repeating group)이 없으며, 행을 유일하게 식별할 수 있는 상태를 말한다. 쉼표로 이어 붙인 문자열이나 `phone1, phone2, phone3` 같은 반복 컬럼은 대표적인 위반 사례다. 이 글은 위반 테이블을 Postgre
By 송민성
1. 개념
제1정규형(First Normal Form, 1NF)은 정규화(normalization)의 첫 단계로, 테이블이 다음 조건을 만족하는 상태를 말한다.
- 모든 컬럼의 값이 원자값(atomic value)이다. 한 칸에 값이 하나만 들어간다.
- 같은 의미의 컬럼이 반복되는 반복 그룹(repeating group)이 없다.
- 각 행을 유일하게 식별하는 기본키(primary key)가 있다.
- 컬럼 순서나 행 순서에 의미를 두지 않는다.
여기서 "원자값"은 절대적인 기준이 아니라 해당 도메인에서 쪼개서 다룰 필요가 있는가로 정해진다. 예를 들어 전화번호 전체를 하나의 값으로만 쓴다면 010-1234-5678은 원자값이다. 하지만 "번호 여러 개"를 한 칸에 담았다면 원자값이 아니다.
위반 예시:
| order_id | customer | items |
|---|---|---|
| 1 | 김철수 | 키보드, 마우스 |
| 2 | 이영희 | 모니터 |
items에 여러 값이 들어 있으므로 1NF 위반이다. 아래처럼 item1, item2 컬럼으로 나누는 방식도 반복 그룹이라서 역시 위반이다.
2. 왜 사용하는가
1NF를 지키지 않으면 쿼리와 무결성 양쪽에서 문제가 생긴다.
- 검색의 어려움: "마우스를 주문한 사람"을 찾으려면 문자열을 파싱해야 한다.
LIKE '%마우스%'는 "마우스패드"까지 잡아낸다. - 인덱스 활용 불가: 쉼표로 이어 붙인 문자열의 일부 값에는 일반 B-tree 인덱스를 효과적으로 쓸 수 없다.
- 갱신 이상(update anomaly): 값 하나를 바꾸려고 문자열 전체를 읽고 고쳐 써야 하며, 동시 수정 시 한쪽 변경이 덮어써질 수 있다.
- 제약조건 적용 불가: 항목 하나하나에 외래키(foreign key),
NOT NULL,CHECK를 걸 수 없다. - 확장성 문제:
item1~item3구조는 4번째 항목이 필요해지는 순간 스키마를 바꿔야 한다. 대부분 행은NULL로 채워진다.
3. 동작 원리
1NF 변환은 "한 칸에 여러 값"을 "여러 행"으로 펼치는 작업이다.
- 다중값 컬럼과 반복 컬럼을 찾는다.
- 그 값들을 별도 테이블로 분리한다. 한 값이 한 행이 된다.
- 원래 테이블의 키를 외래키로 가져와 두 테이블을 연결한다.
- 새 테이블에 기본키를 정한다. 보통
(order_id, item_name)같은 복합키나 대리키(surrogate key)를 쓴다.
변환 후:
orders
| order_id | customer |
|---|---|
| 1 | 김철수 |
| 2 | 이영희 |
order_items
| order_id | item_name |
|---|---|
| 1 | 키보드 |
| 1 | 마우스 |
| 2 | 모니터 |
이 구조에서는 항목 추가가 INSERT 한 번이고, 스키마 변경이 필요 없다. 이 단계에서는 customer처럼 키가 아닌 컬럼의 종속 문제는 다루지 않는다. 그것은 2NF 이후의 주제다.
4. 코드 예제
PostgreSQL 기준으로 위반 스키마와 1NF 스키마를 비교한다.
-- 1) 1NF 위반: 한 칸에 여러 값
CREATE TABLE orders_bad (
order_id integer PRIMARY KEY,
customer text NOT NULL,
items text NOT NULL -- '키보드,마우스'
);
INSERT INTO orders_bad VALUES
(1, '김철수', '키보드,마우스'),
(2, '이영희', '모니터');
-- 2) 1NF 스키마
CREATE TABLE orders (
order_id integer PRIMARY KEY,
customer text NOT NULL
);
CREATE TABLE order_items (
order_id integer NOT NULL REFERENCES orders(order_id),
item_name text NOT NULL,
PRIMARY KEY (order_id, item_name)
);
-- 3) 위반 테이블의 데이터를 행으로 펼쳐 이전
INSERT INTO orders (order_id, customer)
SELECT order_id, customer FROM orders_bad;
INSERT INTO order_items (order_id, item_name)
SELECT order_id, trim(item)
FROM orders_bad,
LATERAL unnest(string_to_array(items, ',')) AS item;
-- 4) 이제 조건 검색이 정확하고 인덱스를 탄다
SELECT o.customer
FROM orders o
JOIN order_items i USING (order_id)
WHERE i.item_name = '마우스';string_to_array로 문자열을 배열로 만들고 unnest로 행으로 펼치는 방식이 PostgreSQL에서 흔히 쓰는 이전 방법이다.
5. 시간 복잡도 또는 성능 특성
정확한 시간복잡도는 데이터 분포와 실행 계획에 따라 달라지므로, 구조적으로 확실한 특성만 정리한다.
order_items의 기본키(order_id, item_name)에는 B-tree 인덱스가 자동 생성된다. 인덱스를 통한 조회는 일반적으로 O(log n)이다.item_name만으로 검색하는 쿼리가 잦다면CREATE INDEX ON order_items (item_name);처럼 별도 인덱스가 필요하다. 복합 인덱스의 선두 컬럼은order_id이기 때문이다.- 쉼표 문자열에 대한
LIKE '%마우스%'는 앞쪽 와일드카드 때문에 일반 B-tree 인덱스를 쓸 수 없고, 보통 전체 행을 훑게 된다. - 분리의 대가로 조회 시
JOIN이 필요하다. 적절한 인덱스가 있으면 대개 감당할 수 있는 비용이다.
6. 실무 사용 사례
- 주문/상품: 주문 한 건에 여러 상품이 들어가는 구조는
orders와order_items로 나누는 것이 표준 패턴이다. - 태그/카테고리:
tags = 'java,spring'대신post_tags(post_id, tag)테이블을 두거나, 태그를 별도 테이블로 만들고 연결 테이블(junction table)로 다대다(N:M) 관계를 표현한다. - 연락처:
phone1, phone2, phone3대신user_phones(user_id, phone_number)로 분리한다. - PostgreSQL의 `ARRAY`, `JSONB`: 이 타입들은 한 칸에 여러 값을 담으므로 엄밀한 1NF 관점에서는 논쟁의 여지가 있다. 항목에 개별 제약조건이나 외래키가 필요 없고, 항상 통째로 읽고 쓰며, 항목 단위로 조인하지 않는다면 실용적 선택이 될 수 있다. 반대로 항목 단위로 검색·조인·무결성 관리가 필요하면 별도 테이블이 맞다.
7. 주의할 점
- "원자값"은 맥락 의존적이다: 주소를 통째로 쓰는 시스템에서는
address한 컬럼도 문제없지만, 시/도별 통계가 필요하다면 쪼개야 한다. 쿼리 요구사항을 기준으로 판단한다. - 컬럼 번호 붙이기는 해결책이 아니다:
item1,item2는 값을 컬럼으로 옮겼을 뿐 반복 그룹이다. - 기본키가 없으면 1NF도 흔들린다: 완전히 동일한 행이 중복 저장될 수 있어 행 식별이 불가능해진다.
- 1NF만으로는 충분하지 않다: 1NF를 만족해도 부분 종속, 이행 종속에 의한 이상은 남아 있다. 이는 2NF, 3NF에서 다룬다.
- 의도적 비정규화와 구분한다: 읽기 성능을 위해 비정규화(denormalization)하는 것은 정규형을 이해한 상태에서 비용을 감수하는 선택이다. 이해 없이 생긴 위반과는 다르다.
8. 핵심 정리
- 1NF는 모든 컬럼이 원자값이고, 반복 그룹이 없으며, 행을 식별하는 기본키가 있는 상태다.
- 쉼표 구분 문자열과
item1, item2같은 반복 컬럼이 대표적인 위반이다. - 해결 방법은 다중값을 별도 테이블의 여러 행으로 분리하고 외래키로 연결하는 것이다.
- PostgreSQL에서는
string_to_array와unnest로 기존 데이터를 행으로 펼쳐 이전할 수 있다. ARRAY,JSONB는 항목 단위 제약·검색·조인이 필요 없을 때만 신중하게 사용한다.- 1NF는 정규화의 출발점이며, 이어서 2NF와 3NF로 종속성 문제를 다룬다.