โ† Database Design

Normalization

~390 words ยท 2 min read

Designing tables that don't lie to you

Normalization is the process of organizing a database's columns and tables to minimize redundancy and avoid update anomalies. The normal forms are a ladder โ€” each level fixes a class of problems the previous one allowed.

First Normal Form (1NF) โ€” atomic values

A table is in 1NF when every column holds a single, atomic value โ€” no lists, no comma-separated tags stuffed into one cell:

-- Violates 1NF: a list crammed into one cell
| user_id | name | phones                    |
| 1       | Ana  | "555-1000, 555-2000"      |

-- 1NF-compliant: one row per phone
| user_id | name | phone      |
| 1       | Ana  | 555-1000   |
| 1       | Ana  | 555-2000   |

Second Normal Form (2NF) โ€” no partial dependencies

A table is in 2NF when it is in 1NF and every non-key column depends on the whole primary key, not just part of it. This only matters for composite keys:

-- Violates 2NF: 'course_name' depends only on course_id, not the full key
PK = (student_id, course_id)
| student_id | course_id | course_name      | enrolled_at |
| 1          | CS101     | Intro to CS      | 2025-01-01  |
| 2          | CS101     | Intro to CS      | 2025-01-02  |   -- 'Intro to CS' repeated!

The fix is to split course_name into its own courses table keyed by course_id.

Third Normal Form (3NF) โ€” no transitive dependencies

A table is in 3NF when non-key columns depend on nothing but the key โ€” no transitive chains where A โ†’ B โ†’ C:

-- Violates 3NF: zip โ†’ city, so city depends on zip, not the user
| user_id | name | zip   | city        |
| 1       | Ana  | 10001 | New York    |
| 2       | Bob  | 10001 | New York    |   -- city duplicated; if 10001's
                                            -- city name changes, we must
                                            -- update many rows (update anomaly)

Split into users(user_id, name, zip) and zip_codes(zip, city). Now the city lives in exactly one place.

The slogan for 3NF, attributed to E.F. Codd: "every non-key attribute must provide a fact about the key, the whole key, and nothing but the key." If a column describes something other than the row's identity, move it to its own table.

When to denormalize

Normalization trades redundancy for consistency: every fact lives in one place, so updates can't disagree. But joins cost performance, and at scale (analytics, reporting, read-heavy workloads) you may deliberately denormalize โ€” duplicate data to avoid joins. Denormalization is acceptable when:

  • Reads vastly outnumber writes (the duplicated data rarely changes).
  • Join cost dominates query latency (data warehouses, dashboards).
  • You accept the complexity of keeping duplicates in sync (triggers, scheduled jobs, or eventual consistency).

Always start normalized; denormalize only with a measured reason.