Think of these are audits or sanity checks that make sure you’ve designed your database properly. Doing things properly means that the query planners and optimizers work well, that you enjoy all the benefits of consistency (the “C” in ACID.)
There are five levels.
The Progression
Each form is the previous one plus one more rule. Every fix is the same move: split a table so that a fact is stored in exactly one place.
Why bother? Redundant facts cause anomalies:
- Update: change a fact in one row, forget the other copies, now the DB disagrees with itself.
- Insert: can’t record a fact without some unrelated fact (e.g. can’t add a patient until they have a diagnosis).
- Delete: deleting one fact accidentally deletes another.
Vocab
- Key: column(s) that uniquely identify a row. Key columns are underlined in the tables below.
- Functional dependency A→B: knowing A tells you B. Each value of A goes with exactly one value of B. E.g. pid→name.
- Multivalued dependency A↠B: knowing A tells you a whole set of Bs, independent of the other columns. E.g. a patient has a set of allergies.
Redundant values are shown in red.
Unnormalized
A spreadsheet someone emailed you:
nameAnaCyAnaage34"seventy-one"34diagnosesflu, asthmacopdflu
Problems: a list crammed into one cell, a string in a number column, and no way to tell whether the two Anas are the same person. Plus, “the last row is the most recent visit” is information hiding in row order.
- Using row order to convey information is not permitted
- Mixing data types within the same column is not permitted
- Having a table without a primary key is not permitted
- Repeating groups are not permitted
In plain terms: it’s an actual relation. One value per cell, one type per column, a key, and row order means nothing. This is just the definition of a relation from Relational Algebra: a set of tuples over fixed domains.
Fix: one row per (patient, diagnosis), store dates explicitly, add IDs. Key is (pid,icd). We’ve also pulled in the patient’s primary care doctor (PCP):
pid1133icdJ11J45J44J11nameAnaAnaCyCypcpDr. LeeDr. LeeDr. KimDr. Kimphone555-0101555-0101555-0199555-0199dateJan 5Feb 10Mar 15Apr 20
Valid 1NF, but Ana’s name and doctor are stored once per diagnosis.
Each non-key attribute in the table must be dependent on the entire primary key.
In plain terms: no column depends on just part of the key. Only matters when the key has multiple columns.
Problem: pid→name,pcp,phone. None of those care about icd. Only date needs the whole key (pid,icd).
Fix: split the patient facts out.
Patientpid123nameAnaBoCypcpDr. LeeDr. LeeDr. Kimphone555-0101555-0101555-0199Diagnosispid1133icdJ11J45J44J11dateJan 5Feb 10Mar 15Apr 20
Ana’s name is now stored once. Bonus: we can now add Bo, who has no diagnoses yet (insert anomaly gone).
Each non-key attribute in the table must depend on the key, the whole key, and nothing but the key.
In plain terms: no column depends on another non-key column. No chains like key→X→Y.
Problem: pid→pcp→phone. The phone belongs to the doctor, not the patient. Dr. Lee’s number is stored once per patient. If it changes, every one of Lee’s patients’ rows needs updating.
Fix: give doctors their own table.
Patientpid123nameAnaBoCypcpDr. LeeDr. LeeDr. KimDoctorpcpDr. LeeDr. Kimphone555-0101555-0199
(Diagnosis unchanged.) This is where most real schemas stop. Mnemonic: “the key, the whole key, and nothing but the key, so help me Codd.” 1NF = there is a key, 2NF = the whole key, 3NF = nothing but the key.
Each attribute in the table must depend on the key, the whole key, and nothing but the key.
In plain terms: if any column determines another, it must be a key. 3NF only polices non-key columns. BCNF polices all of them. The difference only shows up when a non-key column determines part of the key.
New table. Rules: a patient sees one doctor per specialty, and each specialist practices one specialty. Key is (pid,specialty):
pid1133specialtycardiodermcardiodermdoctorDr. ShahDr. ItoDr. ShahDr. Park
Problem: doctor→specialty, but doctor isn’t a key. Passes 3NF (because specialty is part of the key), but “Shah is a cardiologist” is stored twice. Delete Cy’s derm row and you lose the fact that Park is a dermatologist.
Fix:
SpecialistdoctorDr. ShahDr. ItoDr. ParkspecialtycardiodermdermReferralpid1133doctorDr. ShahDr. ItoDr. ShahDr. Park
Trade-off: “one doctor per specialty per patient” is no longer enforceable by a simple key constraint, because the two columns now live in different tables. This is why people sometimes stay at 3NF.
The only kinds of multivalued dependency allowed in a table are multivalued dependencies on the key.
In plain terms: don’t store two independent lists in one table.
Ana has allergies penicillin,peanuts and meds albuterol,oseltamivir. These have nothing to do with each other, but in one table every allergy must be paired with every med. Key is all three columns:
pid1111allergypenicillinpenicillinpeanutspeanutsmedalbuteroloseltamiviralbuteroloseltamivir
No functional dependencies at all, so it’s BCNF. But pid↠allergy and pid↠med. The table is literally a Cartesian product: {penicillin,peanuts}×{albuterol,oseltamivir}. Adding a third med means adding two rows, one per allergy. Forget one and you’ve implied Ana isn’t allergic to something.
Fix: one table per list.
Allergypid11allergypenicillinpeanutsMedicationpid11medalbuteroloseltamivir
4 rows became 2 + 2. With k allergies and j meds it’s k×j vs k+j.
It must not be possible to describe the table as being the logical result of joining some other tables together.
In plain terms: if a table can be rebuilt by joining smaller tables, store the smaller tables instead.
Hospital rule: a doctor performs a procedure at a hospital whenever the doctor knows the procedure, the doctor has privileges at the hospital, and the hospital offers the procedure.
doctorDr. ShahDr. ShahDr. ShahDr. ItoprocedureangioangiostentstenthospitalMercySt. Luke’sMercyMercy
No independent lists (Shah does stents, has St. Luke’s privileges, but doesn’t do stents at St. Luke’s, since they don’t offer them), so it’s 4NF. But because of the rule, this table is exactly the join of three pairwise facts:
SkilldoctorDr. ShahDr. ShahDr. ItoprocedureangiostentstentPrivilegedoctorDr. ShahDr. ShahDr. ItohospitalMercySt. Luke’sMercyOffershospitalMercyMercySt. Luke’sprocedureangiostentangio
Original=Skill⋈Privilege⋈Offers
Problem with the 3-column table: if St. Luke’s starts offering stents, you have to remember to add a row for every doctor who knows stents and has privileges there. In 5NF you add one row to Offers and the join produces the rest.
Subtle bit: you need all three. Joining just Skill⋈Privilege invents a fake row (Shah, stent, St. Luke’s). And if the business rule doesn’t hold (doctors are credentialed per procedure per hospital), the 3-column table is irreducible and already 5NF. 5NF depends on what’s true in the world, not just on what the data looks like.
Summary
| Form | Plain rule | Typical fix |
|---|
| 1NF | It’s a real relation: atomic cells, a key, no row order | One row per fact, add IDs |
| 2NF | No column depends on part of the key | Split off the partial dependency |
| 3NF | No column depends on a non-key column | Split off the chain (X→Y) |
| BCNF | Every determinant is a key | Split off the determinant |
| 4NF | No independent lists in one table | One table per list |
| 5NF | No table that’s just a join of smaller ones | Store the smaller ones |
In practice: aim for 3NF/BCNF in transactional (OLTP) databases. 4NF/5NF problems are rare and usually get fixed by common sense. Analytical databases (data warehouses, star schemas) often denormalize on purpose, trading redundancy for fewer joins.