Database Design
Normalisation
Split tables until every fact is stored in
JrCodex·8 min read
Jr Codex DBMS Notes
Level: Intermediate Prerequisites: Chapter 3: Functional Dependencies Time to complete: ~25 minutes
Table of Contents
- What Normalisation Is
- First Normal Form
- Second Normal Form
- Third Normal Form
- Boyce-Codd Normal Form
- Lossless Decomposition
- How Far to Go
- Summary & Next Steps
1. What Normalisation Is
The Procedure
─────────────────────────────────────────
Split tables until every fact is stored in
exactly ONE place, determined by a key.
Each normal form removes one specific kind of
bad dependency:
1NF non-atomic values
2NF partial dependencies
3NF transitive dependencies
BCNF any determinant that is not a super key
Each form ASSUMES the previous one. They are
cumulative, not alternatives.
─────────────────────────────────────────
We normalise the broken enrollments_bad table from Chapter 3 step by step.
2. First Normal Form
1NF: every value is atomic; no repeating groups.
The Violation
─────────────────────────────────────────
students_bad
┌──────┬────────┬──────────────────────┐
│ s_id │ name │ phones │
├──────┼────────┼──────────────────────┤
│ 1 │ Asha │ "9876, 9123" │ ← two values, one cell
│ 2 │ Ravi │ "9000" │
└──────┴────────┴──────────────────────┘
─────────────────────────────────────────
Why It Is Genuinely Broken
─────────────────────────────────────────
- "find the student with phone 9123" needs LIKE
'%9123%', which cannot use an index
(Module 3, Chapter 2)
- and matches 89123 too
- you cannot constrain a phone's format
- adding a third phone means string surgery
- counting phones means parsing
─────────────────────────────────────────
-- 1NF: the multivalued attribute becomes its own table (Chapter 2, Rule 6).
CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE student_phones (
student_id INTEGER NOT NULL REFERENCES students(id) ON DELETE CASCADE,
phone TEXT NOT NULL,
PRIMARY KEY (student_id, phone)
);The Modern Caveat
─────────────────────────────────────────
JSON and ARRAY columns technically violate 1NF,
and every major database now offers them.
Legitimate uses: genuinely schemaless payloads,
audit blobs, external API responses stored whole.
Not legitimate: anything you will filter, join
or constrain on. If you find yourself indexing
inside the JSON, it wanted to be a table.
─────────────────────────────────────────
3. Second Normal Form
2NF: in 1NF, and no partial dependency — no non-key attribute depends on only part of a composite key.
The Violation
─────────────────────────────────────────
enrollments_bad(s_id, c_id, s_name, c_title, grade)
KEY = {s_id, c_id}
s_id → s_name ← only PART of the key
c_id → c_title ← only PART of the key
{s_id,c_id} → grade ← the FULL key. Fine.
So s_name is repeated for every course the
student takes, and c_title for every student on
the course.
─────────────────────────────────────────
-- 2NF: move each partially-dependent attribute to a table keyed by its determinant.
CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE courses (id INTEGER PRIMARY KEY, title TEXT NOT NULL, dept_id INTEGER);
CREATE TABLE enrollments (
s_id INTEGER NOT NULL REFERENCES students(id),
c_id INTEGER NOT NULL REFERENCES courses(id),
grade TEXT,
PRIMARY KEY (s_id, c_id) -- only the FULLY dependent attribute remains
);The Shortcut
─────────────────────────────────────────
2NF violations are only possible when the primary
key is COMPOSITE.
With a single-column key, no attribute can depend
on "part" of it — there are no parts. Any table
with a surrogate integer key is automatically in
2NF.
This is one more argument for surrogate keys
(Module 2, Chapter 2).
─────────────────────────────────────────
4. Third Normal Form
3NF: in 2NF, and no transitive dependency — no non-key attribute determines another non-key attribute.
The Violation
─────────────────────────────────────────
courses(c_id, title, dept_id, dept_name)
KEY = {c_id}
c_id → dept_id key determines non-key ✓
dept_id → dept_name NON-key determines
NON-key ✗
So c_id → dept_name TRANSITIVELY, and dept_name
is repeated on every course in that department.
Rename a department and you must update every
one of its courses — the update anomaly, back
again.
─────────────────────────────────────────
-- 3NF: the transitively-dependent attribute moves to a table keyed by its determinant.
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE courses (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
credits INTEGER NOT NULL CHECK (credits > 0),
dept_id INTEGER NOT NULL REFERENCES departments(id) -- dept_name lives ONCE
);The Memorable Statement
─────────────────────────────────────────
"Every non-key attribute must depend on THE KEY,
THE WHOLE KEY, and NOTHING BUT THE KEY."
the key ──► 1NF (there is a key at all)
the WHOLE key ──► 2NF (no partial dependency)
NOTHING BUT ──► 3NF (no transitive
the key dependency)
3NF is the practical target for most schemas.
─────────────────────────────────────────
5. Boyce-Codd Normal Form
BCNF: for every non-trivial FD X → Y, X is a super key.
Stricter Than 3NF
─────────────────────────────────────────
3NF permits one exception: a non-key determinant
is allowed if the attribute it determines is part
of SOME candidate key.
BCNF removes that exception entirely. Every
determinant must be a super key. No exceptions.
The two forms differ only when a table has
OVERLAPPING candidate keys — uncommon, but real.
─────────────────────────────────────────
The Classic Example
─────────────────────────────────────────
tutoring(student, subject, tutor)
Rules:
- each tutor teaches exactly ONE subject
- a student has one tutor per subject
FDs:
{student, subject} → tutor candidate key
tutor → subject ← determinant,
NOT a super key
In 3NF? Yes — `subject` is part of a candidate
key, so the exception applies.
In BCNF? No — `tutor` is not a super key.
The anomaly: you cannot record that a new tutor
teaches Physics until some student is assigned
to them.
─────────────────────────────────────────
-- BCNF: decompose on the offending determinant.
CREATE TABLE tutors (
tutor TEXT PRIMARY KEY,
subject TEXT NOT NULL -- tutor → subject, now with tutor as the KEY
);
CREATE TABLE student_tutors (
student TEXT NOT NULL,
tutor TEXT NOT NULL REFERENCES tutors(tutor),
PRIMARY KEY (student, tutor)
);The Cost of BCNF
─────────────────────────────────────────
BCNF decomposition is always LOSSLESS but not
always DEPENDENCY-PRESERVING.
Here, the FD {student, subject} → tutor now spans
two tables, so the database can no longer enforce
it with a single constraint — it needs a trigger
or application logic.
3NF is always both lossless AND
dependency-preserving. That is the trade, and it
is why 3NF is usually where people stop.
─────────────────────────────────────────
6. Lossless Decomposition
A split is only valid if joining the pieces reproduces the original exactly.
The Test
─────────────────────────────────────────
Decomposing R into R1 and R2 is LOSSLESS if:
(R1 ∩ R2) → R1 or (R1 ∩ R2) → R2
In words: the shared attributes must be a KEY of
at least one of the two pieces.
─────────────────────────────────────────
Why It Matters
─────────────────────────────────────────
A LOSSY split silently invents rows on rejoin.
Split (student, subject, tutor) into
(student, subject) and (subject, tutor), sharing
only `subject` — which is a key of neither.
Rejoin, and every student studying Physics is
paired with every Physics tutor. You get MORE
rows than you started with, and they are false.
Always decompose on a DETERMINANT — then the
shared attribute is a key of the new table by
construction, and losslessness is automatic.
─────────────────────────────────────────
7. How Far to Go
The Higher Forms
─────────────────────────────────────────
4NF removes multivalued dependencies —
two independent multivalued facts in one
table, which multiply combinatorially
5NF removes join dependencies
6NF irreducible; used in temporal databases
Rarely needed. If you designed from an ER model
(Chapters 1-2) you usually land in 4NF without
trying, because independent multivalued facts
naturally became separate entities.
─────────────────────────────────────────
The Practical Target
─────────────────────────────────────────
NORMALISE TO 3NF (or BCNF where it is free).
Go further only when a specific anomaly is
actually biting you.
Then DENORMALISE deliberately, measured, and
documented — Chapter 5.
─────────────────────────────────────────
The Honest Note on Performance
─────────────────────────────────────────
"Normalisation is slow because of all the joins"
is mostly folklore.
Indexed joins on integer keys are fast
(Module 5), and normalised tables are SMALLER, so
more rows fit in the buffer pool and fewer pages
are read.
Normalised designs frequently outperform
denormalised ones on write-heavy workloads,
because there is only one copy to update.
Measure before assuming otherwise.
─────────────────────────────────────────
8. Summary & Next Steps
Key Takeaways
- Each normal form removes one kind of bad dependency, and they are cumulative: atomicity, then partial, then transitive, then any non-super-key determinant.
- 2NF violations require a composite key, so a surrogate single-column key puts a table in 2NF automatically.
- BCNF is stricter than 3NF but is not always dependency-preserving, which is why 3NF is the usual stopping point.
- Always decompose on a determinant — this makes the split lossless by construction, whereas splitting on a non-key silently invents rows on rejoin.
Concept Check
- Why can a table with a single-column primary key never violate 2NF?
- In the tutoring example, what specifically can you not record before decomposing to BCNF?
- What does a lossy decomposition do when you rejoin the pieces, and what rule prevents it?
Next Chapter
→ Chapter 5: When to Denormalise
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index