Normalization: 1NF, 2NF, 3NF and When Denormalization Is Justified
coreintermediate1NF: every column holds one atomic value, no repeating groups. 2NF: every non-key column depends on the whole primary key, not part of it. 3NF: every non-key column depends only on the key, not on another non-key column. Denormalization deliberately breaks one of these for a specific, measured performance or simplicity reason.
Think of it as
Each normal form eliminates one specific way redundancy creeps in. 1NF stops you from stuffing a list into one column. 2NF stops a composite-key table from storing a fact that really only depends on part of the key (which then repeats needlessly per other-part-of-key value). 3NF stops a table from storing a fact that depends on another non-key column rather than the key itself (which then has to be updated in multiple places to stay consistent). Denormalization is the informed choice to accept one of these redundancies anyway, because the read-performance or simplicity benefit outweighs the update-anomaly risk for that specific case.
What we're doing: Show a 3NF violation causing an update anomaly (two rows disagreeing about the same fact), then the normalized fix that makes the anomaly structurally impossible.
- 2
- Both orders share zip 10001, and both should always agree on the city that zip maps to.
- 4
- Updating just one row's city leaves the schema in an inconsistent state — nothing prevents zip and city from disagreeing across rows.
- 7–8
- The same zip code now maps to two different city spellings — a direct consequence of storing city redundantly instead of deriving it from zip via a lookup.
zip | city
-------+----------
10001 | NYC
10001 | New York
(2 rows)Why this works: Nothing in the orders_denorm schema enforces that every row with zip = '10001' agrees on city, because city is stored independently per row rather than derived from a single authoritative source — this is exactly the transitive dependency 3NF is defined to eliminate. Moving city into its own zip_codes table keyed by zip makes the inconsistency structurally impossible: there would be exactly one row for zip 10001, and every order referencing it automatically sees the same, single value.
Denormalizing without a specific, measured reason
Wrong
Better
What you see: A schema accumulates redundant copies of the same fact across many tables "for convenience" or "in case it's faster," with no actual measurement showing the normalized version was too slow — and now every one of those copies needs separate logic to stay in sync.
Why: Denormalization is a real, valid engineering trade-off, but it trades update simplicity and consistency for read performance — a trade that is only worth making when a specific, measured read path actually needs it. Applying it by default, everywhere, without measurement, accepts the update-anomaly risk (as demonstrated above) without ever collecting the performance benefit it was supposed to be worth.
- Denormalized (3NF violation)
- orders stores both zip and city directly
- city is really determined by zip, not by the order itself
- updating a zip code's city means updating every order row that used it
- Normalized (3NF)
- orders stores only zip
- a separate zip_codes table maps zip -> city, once
- updating a city means changing exactly one row
What each normal form eliminates
Together
Remember: 1NF/2NF/3NF each eliminate one specific kind of redundancy; a 3NF violation shows up as the same fact stored in multiple rows that can silently disagree. Denormalize deliberately, for a specific measured reason — not by default.
See also: entities relationships cardinality · constraints prevent duplicate orphaned invalid data

