> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MySQL의 정규화

> MySQL 데이터베이스 스키마를 정규화하는 방법: 제1, 제2, 제3 정규형을 예시로 설명하고, 비정규화가 합리적인 경우를 다룹니다.

> [card] [VillageSQL is a drop-in replacement for MySQL with extensions.](https://villagesql.com/docs/en/8.4/stable/install-server.md)
>
> All examples on this page work on VillageSQL. Install Now →

데이터베이스 정규화는 중복을 줄이고 갱신 이상을 막기 위해 테이블을 구성하는 과정입니다. 대부분의 애플리케이션은 제3 정규형(3NF)에 도달해야 합니다. 그 너머에서는 이득은 대부분 이론적이고 조인 복잡성은 실재합니다.

## 제1 정규형(1NF)

모든 열이 원자 값(셀당 하나의 값)을 담고 반복 그룹이 없을 때 테이블은 1NF입니다.

**1NF 위반** — 여러 값이 한 열에 묶여 있음:

```sql theme={null}
CREATE TABLE orders (
    id          INT PRIMARY KEY,
    customer    VARCHAR(100),
    product_ids VARCHAR(255)  -- "1,4,7" — not atomic
);
```

**1NF 충족** — 셀당 하나의 값, 반복 그룹은 자식 테이블로 추출:

```sql theme={null}
CREATE TABLE orders (
    id       INT PRIMARY KEY,
    customer VARCHAR(100)
);

CREATE TABLE order_items (
    order_id   INT,
    product_id INT,
    PRIMARY KEY (order_id, product_id)
);
```

## 제2 정규형(2NF)

1NF이면서 모든 비키 열이 기본 키의 일부가 아니라 **전체**에 종속될 때 테이블은 2NF입니다. 이는 복합 기본 키를 가진 테이블에만 적용됩니다.

**2NF 위반** — `product_name`이 전체 `(order_id, product_id)` 키가 아니라 `product_id`에만 종속됨:

```sql theme={null}
CREATE TABLE order_items (
    order_id     INT,
    product_id   INT,
    product_name VARCHAR(100),  -- partial dependency: only needs product_id
    quantity     INT,
    PRIMARY KEY (order_id, product_id)
);
```

**2NF 충족** — `product_name`을 자체 테이블로 옮김:

```sql theme={null}
CREATE TABLE products (
    id   INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE order_items (
    order_id   INT,
    product_id INT,
    quantity   INT,
    PRIMARY KEY (order_id, product_id),
    FOREIGN KEY (product_id) REFERENCES products (id)
);
```

## 제3 정규형(3NF)

2NF이면서 어떤 비키 열도 다른 비키 열에 종속되지 않을 때 테이블은 3NF입니다. 이런 종속을 이행적 종속이라고 합니다.

**3NF 위반** — `city`와 `state`가 `customer_id`에 직접이 아니라 `zip_code`에 종속됨:

```sql theme={null}
CREATE TABLE customers (
    id       INT PRIMARY KEY,
    name     VARCHAR(100),
    zip_code VARCHAR(10),
    city     VARCHAR(100),  -- transitively depends on zip_code
    state    CHAR(2)        -- transitively depends on zip_code
);
```

**3NF 충족** — 이행적 종속을 추출:

```sql theme={null}
CREATE TABLE zip_codes (
    zip_code VARCHAR(10) PRIMARY KEY,
    city     VARCHAR(100),
    state    CHAR(2)
);

CREATE TABLE customers (
    id       INT PRIMARY KEY,
    name     VARCHAR(100),
    zip_code VARCHAR(10),
    FOREIGN KEY (zip_code) REFERENCES zip_codes (zip_code)
);
```

실제로는 우편번호를 별도 테이블로 추출하는 것은 대부분의 애플리케이션에 과합니다. 모든 엣지 케이스에서의 기계적인 준수보다 원칙이 더 중요합니다.

## 비정규화해야 할 때

정규화는 쓰기 무결성을 높이고 중복을 줄이지만, 읽기에 조인 오버헤드를 더합니다. 읽기 중심이나 분석 워크로드에서는 때때로 의도적인 비정규화가 타당합니다.

| 시나리오 | 정규화 | 비정규화 |
| :- | :- | :- |
| 잦은 쓰기, 데이터 무결성이 중요 | 예 | — |
| 6개 이상의 테이블을 조인하는 보고 쿼리 | — | 고려 |
| 작은 데이터셋(\< 100K 행) | 예 | 불필요 |
| 데이터 웨어하우스 또는 분석 복제본 | — | 예 |
| 읽기/쓰기가 섞인 OLTP 애플리케이션 | 예 | 핫 패스만 |

비정규화하기 전에 인덱스를 확인하세요. 느린 조인 문제의 대부분은 인덱스 누락 문제입니다. 조인 열에 커버링 인덱스를 추가하는 것으로 충분한 경우가 많습니다. [MySQL 인덱스의 동작 방식](https://villagesql.com/docs/ko/guides/mysql-indexes.md)을 참고하세요.

비정규화는 애플리케이션이 데이터를 쓰는 바로 그 테이블이 아니라 읽기 복제본이나 OLAP 저장소에서 가장 잘 동작합니다.

## 자주 묻는 질문

#### 3NF 너머까지 가야 하나요?

대부분의 애플리케이션에는 3NF로 충분합니다. 보이스-코드 정규형(BCNF)은 겹치는 후보 키가 3NF에서도 여전히 이상을 허용하는 엣지 케이스를 다루는데, 학술적 연습 밖에서는 거의 생기지 않는 상황입니다. 제거하려는 특정 이상이 있는 것이 아니라면 3NF에서 멈추세요.

#### 성능을 위해 비정규화해야 하나요?

구조를 바꾸기 전에 프로파일링하세요. 대부분의 느린 쿼리는 조인이 아니라 인덱스 누락 때문입니다. 먼저 인덱스를 추가하세요. 인덱스가 잘 걸린 쿼리가 큰 테이블에서 여전히 느리다면, 트랜잭션 데이터베이스가 아니라 읽기 복제본이나 구체화된 요약 테이블에서의 비정규화를 고려하세요.

#### 2NF 위반과 3NF 위반의 차이는 무엇인가요?

2NF 위반은 기본 키의 *일부*에 종속되는 열입니다(복합 PK에서만 가능). 3NF 위반은 *다른 비키 열*에 종속되는 열입니다. 2NF는 부분 키 종속에 관한 것이고, 3NF는 비키 열을 거치는 이행적 종속에 관한 것입니다.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| 여러 행에 걸쳐 중복 데이터가 나타남 | 2NF 또는 3NF 위반일 가능성 — 반복되는 데이터를 자체 테이블로 옮기기 |
| 갱신 이상(한 행을 바꾸면 다른 행이 낡은 값으로 남음) | 이행적 종속 — 조회 테이블로 추출 |
| 5개 이상의 테이블을 조인하는 쿼리가 느림 | FK 열의 인덱스를 먼저 확인하고, 인덱스가 도움이 되지 않을 때만 비정규화 |
| 정규화된 스키마가 쿼리하기에 너무 복잡함 | 읽기에 최적화된 뷰나 비정규화된 분석 복제본 고려 |

## 함께 보기

* [MySQL 데이터 타입 선택하기](https://villagesql.com/docs/ko/guides/choosing-data-types.md) — 스키마를 정규화한 뒤 올바른 타입 고르기
* [MySQL의 외래 키](https://villagesql.com/docs/ko/guides/foreign-keys.md) — 정규화가 만드는 관계 강제하기
* [MySQL의 CHECK 제약 조건](https://villagesql.com/docs/ko/guides/check-constraints.md) — 외래 키 무결성 너머의 추가 규칙
* [Sakila 샘플 데이터베이스](https://villagesql.com/docs/ko/tutorial/sakila-database.md) — 연습할 수 있는 정규화된 스키마


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.