정규화의 핵심은 중복 제거이다.
한 테이블에서 중복된 데이터가 많이 나온다면 이후에 수정같은 작업을 해야할 때 모든 행에서 같은 데이터를 일일이 찾아 작업해야 한다. 이런 과정은 이후에 이상 현상을 일으킬 수 있다. 이를 방지하기 위해 중복 데이터가 발생할지 미리 예측하고, 테이블을 분리해놓는 작업이 필요하다.
모델링을 할 때 테이블을 만들었다고 가정하고 데이터를 넣어 시뮬레이션 해 본다.
예를 들어 커뮤니티 사이트의 게시글 테이블을 만든다고 가정하자.
아래의 게시글 테이블을 보면 작성자에 짱구가 중복으로 여러 번 등장하는 것을 볼 수 있다. 만약 짱구의 이름을 맹구로 바꾼다면 세 개의 짱구를 맹구로 수정해줘야 한다. 지금은 세 개지만 만약 이런 데이터가 몇 백, 몇 천개라면 작업을 하는데 시간도 오래 걸리고, 실수가 발생할 수 있어 비효율적이다.
posts Table
| id | 제목 | 내용 | 작성자 |
|---|---|---|---|
| 1 | 안녕하세요 | 반갑습니다 | 짱구 |
| 2 | 오랜만입니다 | 시험 끝나고 왔습니다 | 짱구 |
| 3 | 요즘 날씨 | 좋죠??? | 짱구 |
이 때 작성자를 다른 테이블로 분리한 뒤 Foreign key로 가져오면 이 문제는 해결된다. Foreign key로 데이터를 가져올 경우 중복되는 데이터(작성자 id) 1은 중복으로 치지 않는다.
posts Table
| id | 제목 | 내용 | 작성자 id(FK) |
|---|---|---|---|
| 1 | 안녕하세요 | 반갑습니다 | 1 |
| 2 | 오랜만입니다 | 시험 끝나고 왔습니다 | 1 |
| 3 | 요즘 날씨 | 좋죠??? | 1 |
users Table
| id | 제목 |
|---|---|
| 1 | 짱구 |
위와 같이 정규화를 마칠 경우 짱구의 이름을 맹구로 바꿔야 한다면 users Table의 짱구 데이터를 맹구로 변경해주면 된다. 한 번의 수정으로 끝나는 것이다.
시간도 단축되고 실수가 발생할 확률이 거의 없어진다.
그렇다면 아래와 같은 테이블이 있다고 하자.
제목, 내용, 작성자의 데이터가 모두 중복되고 있다.
posts Table
| id | 제목 | 내용 | 작성자 |
|---|---|---|---|
| 1 | 안녕하세요 | 반갑습니다 | 짱구 |
| 2 | 안녕하세요 | 반갑습니다 | 짱구 |
| 3 | 안녕하세요 | 반갑습니다 | 짱구 |
하지만 사실 위에서 '진짜 중복'된 데이터는 작성자 데이터인 짱구 뿐이다.
작성자는 다른 테이블로 분리해야 하지만 제목과 내용은 그렇지 않다.
이 때 제목과 내용 데이터를 '가짜 중복' 이라고 표현한다.
테이블을 제대로 분리하려면 진짜 중복과 가짜 중복을 가려낼 수 있어야 한다.
이 때 다음과 같이 질문해본다.
중복된 데이터 중 하나가 변경되면 나머지의 데이터도 변경해야 하는가?
진짜 중복
위의 게시글 테이블에서 작성자인 짱구가 맹구로 닉네임을 바꾼다고 가정해보자.
한 명의 작성자인 짱구가 게시글 3개를 모두 작성했으므로 각각의 게시물 작성자를 모두 맹구로 바뀌어야한다.
가짜 중복
짱구가 작성한 3개의 게시글은 각각의 제목과 내용을 가진다. 작성자와는 다르게 하나의 게시글에서 제목과 내용 데이터를 변경하면 해당 게시글의 제목과 내용만 변경될 뿐, 다른 게시글의 제목과 내용에는 영향을 미치지 않고 독립적이다.
게시글의 제목과 내용이 중복되는 것은 그저 우연의 일치일 뿐, 정규화 작업 대상이 되지 않는다.
이렇게 미리 시뮬레이션을 돌려보며 컬럼의 데이터들이 독립적인지 아닌지 구분해내면 진짜 중복을 찾을 수 있다.
다른 예시 )
각각 자신의 시그니처 밤양갱을 판매하는 세 가게가 입점한 온라인 쇼핑몰의 데이터베이스를 구축한다고 가정할 때, 아래와 같은 테이블이 있고 상품명 카테고리에 중복이 있는 것을 확인할 수 있다.
stores Table
| id | 가게명 | 상품명 | 카테고리 |
|---|---|---|---|
| 1 | 달코미 | 밤양갱 | 식품 |
| 2 | 행복한먹거리 | 밤양갱 | 식품 |
| 3 | 스낵월드 | 밤양갱 | 식품 |
진짜 중복을 찾기 위해 시뮬레이션을 돌려본다.
진짜 중복
카테고리의 경우 '식품'이 중복되는데, 만약 이 '식품'을 '푸드'라는 이름으로 바꾼다고 가정 해 보자. 위의 밤양갱은 모두 식품 카테고리에 속하므로 식품의 카테고리 이름이 푸드로 바뀐다면 세 개의 카테고리를 모두 '푸드'로 바꿔줘야 할 것이다. 그러므로 카테고리 컬럼의 데이터에 중복이 있을 경우 진짜 중복이라고 할 수 있다.
가짜 중복
상품명의 경우 '밤양갱'이 중복된다. 만약 달코미 가게에서 상품명 '달디단 밤양갱' 으로 바꾼다고 가정 해 보자. 이 밤양갱 상품은 달코미의 시그니처 밤양갱으로, 다른 가게의 밤양갱과는 독립된 개체이다. 그러므로 다른 가게의 밤양갱 상품명이 같이 바뀌면 안 되고 달코미의 밤양갱만 바꿔줘야 한다. 그러므로 상품명 컬럼의 데이터는 중복이 있더라도 가짜 중복이라고 할 수 있다.
위에서 도출된 결과에 따라 테이블을 분리한 뒤 Foreign key를 넣어준다.
stores Table
| id | 가게명 | 상품명 | 카테고리 id (FK) |
|---|---|---|---|
| 1 | 달코미 | 밤양갱 | 1 |
| 2 | 행복한먹거리 | 밤양갱 | 1 |
| 3 | 스낵월드 | 밤양갱 | 1 |
categories Table
| id | 카테고리 |
|---|---|
| 1 | 식품 |
마지막으로 숨어있는 중복을 찾아 정규화 한다.
예시)
아래에 게시글, 사용자, 좋아요 테이블이 있다.
짱구와 철수가 1번 게시글에 모두 좋아요를 클릭해 좋아요 수는 2이다.
posts Table
| id | 제목 | 내용 | 좋아요 수 | 사용자 id(FK) |
|---|---|---|---|---|
| 1 | 안녕하세요 | 반갑습니다 | 2 | 1 |
users Table
| id | 제목 |
|---|---|
| 1 | 짱구 |
| 2 | 철수 |
likes Table
| id | 사용자 id | 게시글 id |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 1 |
얼핏 보면 별 문제가 없어 보이지만 이런 경우를 생각해 보자.
철수가 클릭했던 좋아요를 다시 클릭해 해제할 경우, 좋아요 테이블에서는 2번째 행이 삭제 될 것이다. 그럼 게시글 테이블에서도 좋아요 수가 2에서 1로 바뀌어야 한다. 이렇게 어떤 데이터를 변경할 시 다른 데이터도 변경해야 하는 경우 숨은 중복이라고 표현하겠다.
이 때 위에 언급했던 질문을 다시 상기해 보자.
중복된 데이터 중 하나가 변경되면 나머지의 데이터도 변경해야 하는가?
이 예시에서는 중복된 데이터는 아니지만, 어떤 데이터를 변경할 시 다른 데이터도 변경해야 하는 경우이므로 같은 경우로 생각한다.
좋아요 테이블에서 2번째 행이 삭제되더라도 게시글 테이블에서 변경된 좋아요 수가 반영되지 않는 현상이 일어날 수 있기 때문이다.
보통 이런 숨은 중복은 합계, 평균과 같은 통계 데이터에 해당한다.
'좋아요 수' 컬럼을 삭제해 다음과 같이 정규화 한다.
posts Table
| id | 제목 | 내용 | 사용자 id(FK) |
|---|---|---|---|
| 1 | 안녕하세요 | 반갑습니다 | 1 |
users Table
| id | 제목 |
|---|---|
| 1 | 짱구 |
| 2 | 철수 |
likes Table
| id | 사용자 id | 게시글 id |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 1 |
좋아요 수와 같은 통계 데이터는 테이블로 표현하지 않는 대신, 좋아요 테이블에서 게시글 id가 x인 데이터의 갯수를 구하는 쿼리문으로 구해주면 된다.
SELECT COUNT(*)
FROM likes
WHERE 게시글_id = 1;
이 게시글은 박재성님의 비전공자도 이해할 수 있는 DB 설계 입문/실전 강의를 토대로 작성되었습니다.
https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-db-%EC%84%A4%EA%B3%84-%EC%9E%85%EB%AC%B8/dashboard