Normal Forms in Four Moves
1NF to BCNF with one tiny table at each step. Each form fixes one kind of repeated data: crowded cells, half-key columns, tag-along columns and non-key deciders.
Normalization means store each fact once. When a fact lives in many rows, one change becomes many writes and the copies drift apart. There are four steps and each step fixes one problem.
1NF: one value per cell
| student | phone |
|---|---|
| Rahim | 017..., 018... |
Two phones live in one cell. You cannot search or join on half a cell. Give each phone its own row:
| student | phone |
|---|---|
| Rahim | 017... |
| Rahim | 018... |
2NF: the column needs the whole key
Only matters when the key has two parts. A grade sheet keyed by (student, course):
| student | course | student_name | grade |
|---|---|---|---|
| S1 | Math | Rahim | A |
| S1 | English | Rahim | B |
grade belongs to the pair. Rahim's Math grade and English grade are different. But student_name belongs to the student alone. The course does not change their name, so the name gets written again on every course row. Fix a spelling in the name and you must fix many rows. Move names to students(student, name) and keep only the id here.
After the split, students holds the name once:
| student | name |
|---|---|
| S1 | Rahim |
And the grade sheet keeps only the id:
| student | course | grade |
|---|---|---|
| S1 | Math | A |
| S1 | English | B |
A name change is now one row. Every join sees the new name at once.
3NF: no tag-along columns
Every column has to depend on the key, not on another normal column.
| person | zip | city |
|---|---|---|
| Rahim | 1207 | Dhaka |
| Karim | 1207 | Dhaka |
City does not belong to the person. It belongs to the zip. Zip 1207 is always Dhaka, so Dhaka gets written again and again. Type it wrong once and the same zip now shows two cities. Fix: keep only zip in this table and save the city one time in zips(zip, city).
BCNF: every decider is a key
BCNF stands for Boyce-Codd Normal Form, a slightly stricter 3NF. Anything that decides another column must itself be a key.
A teaching table keyed by (student, subject):
| student | subject | teacher |
|---|---|---|
| S1 | Math | Mr. Karim |
| S2 | Math | Mr. Karim |
Subject does not belong to the pair. It belongs to the teacher. Mr. Karim always teaches Math, so Math gets written again for every student of his. The problem: teacher decides subject but teacher is not a key here. Move Mr. Karim to a new subject and you must fix every student row.
After the split, teachers holds the subject once:
| teacher | subject |
|---|---|
| Mr. Karim | Math |
And lessons keeps who studies with whom:
| student | teacher |
|---|---|
| S1 | Mr. Karim |
| S2 | Mr. Karim |
The subject now lives in one row. Change it there and every student row stays untouched.
Where I stop
In production I design to 3NF by default. BCNF earns its keep only when a non-key column decides things and causes real update bugs. And I denormalize on purpose for read-heavy paths, with the normalized tables staying the source of truth.
Quick recall:
| Form | Fixes | Ask yourself |
|---|---|---|
| 1NF | crowded cells | one value per cell? |
| 2NF | half-key columns | does any column need only part of the key? |
| 3NF | tag-along columns | does a normal column decide another? |
| BCNF | non-key deciders | is every decider a key? |