Suppose every order in your database stores the customer's full shipping address right alongside the order. It feels convenient — everything you need to print a label is in one row. But that same address is now copied onto every order that customer ever placed. The same fact lives in dozens of places at once.
The problem: the same fact, duplicated everywhere
The trouble starts the moment the customer moves. To update their address you'd have to find and change every order row that mentions it. Miss one — or update some rows but crash before finishing — and you now have two different "truths" for the same person.
This is called an update anomaly: because a fact is stored redundantly, changing it requires touching many rows, and any row you miss becomes stale and contradicts the others. Redundancy doesn't just waste space; it quietly lets your data drift out of agreement with itself.
How it works
Normalization is the practice of organizing data so that each fact is stored in exactly one place. Instead of one giant table that repeats the customer's address on every order, you split the data into separate related tables: a customers table that holds each customer's address once, and an orders table where each order simply references its customer.
That reference is a foreign key — a small value (like a customer ID) that points from one table to the row it belongs with in another. To reassemble the full picture for a report or a shipping label, the database follows the foreign key and joins the tables back together at read time.
- OrdersReferences a customer by key instead of copying their details into every row.
Database people describe how far you've gone down this path with normal forms, a numbered ladder of rules. At a high level: 1NF (first normal form) says each cell holds a single value, not a list crammed into one field. 2NF says every column must depend on the whole key of its table, not just part of it. 3NF says non-key columns must depend only on the key — not on each other. You don't need to memorize the formal definitions; the intuition is enough: each step removes another kind of duplication, so each fact ends up living in exactly one home.
A quick gut check: if you ever find yourself updating the same piece of information in more than one row to keep it correct, that's a strong signal the data isn't normalized. The fix is usually to pull that repeated information out into its own table and point to it with a foreign key.
The benefits — and the cost
Normalization buys you three big things. No redundancy: each fact is stored once, so there's no wasted space and no chance of copies disagreeing. No update anomalies: to change an address you update a single row, and every order that references it instantly sees the new value. Integrity: foreign keys let the database enforce that an order can't reference a customer who doesn't exist.
The cost shows up at read time. Because the data is spread across tables, answering a question often means joining several of them back together. Joins are work — and on large tables, under heavy traffic, that work can make reads noticeably slower than scanning a single pre-assembled table.
Denormalization: duplication on purpose
Denormalization is the deliberate reverse: you reintroduce some duplication so that a hot read path doesn't have to join. For example, you might copy a product's name onto each order line so an order summary can be displayed without joining back to the products table. You're knowingly storing a fact in two places to make a common read faster.
Conceptually this is a lot like a cache: you keep a redundant copy of data closer to where it's needed to cut down on work at read time. And it carries the same bargain — you trade away storage and a slice of integrity (the copies can drift apart) in exchange for lower latency.
Denormalization is a trade-off, not an upgrade. The duplicated copies can fall out of sync exactly like the order-address problem you normalized away — you've reintroduced the update anomaly on purpose. Only do it for a read path you've measured and found too slow, and make sure something is responsible for keeping the copies updated when the source changes.
When to choose each
A good default is to normalize first. For write-heavy and transactional systems — anything where correctness matters and data changes often — a clean normalized schema keeps your data trustworthy and your updates simple, which is usually worth the join cost. (See how databases protect those updates in ACID transactions.)
Reach for denormalization selectively, on the specific read paths that are demonstrably hot and too slow when normalized. Treat it as a targeted performance optimization with a known cost — not as the way you design the whole database.