Database normalization is the process of organizing tables so that each piece of data lives in exactly one place. Done well, it eliminates the update anomalies that plague poorly designed schemas — the kind of bugs where changing a customer's address in one row leaves five other rows with the old address, simply because the data was duplicated across the table in the first place.
Normalization is usually described through a series of "normal forms," each one tightening the rules a bit further. You rarely need to go further than the third normal form (3NF) for most applications, so that's where this guide stops.
The Problem: An Unnormalized Table
Imagine a single table tracking orders, storing the customer's details directly on every row:
-- orders (unnormalized)
order_id | customer_name | customer_city | product | price
1 | Alice Smith | Chicago | Keyboard | 49.99
2 | Alice Smith | Chicago | Mouse | 19.99
3 | Bob Jones | Austin | Monitor | 229.99
Alice's name and city are repeated on every order she places. If she moves to a new city, you'd need to update every single row that mentions her — miss one, and your data is now inconsistent with itself.
First Normal Form (1NF): Atomic Values, No Repeating Groups
1NF requires that every column hold a single, indivisible value — no comma-separated lists crammed into one cell, and no repeating groups of columns like product1, product2, product3. The table above already satisfies 1NF since every cell holds one value, but it still has a structural problem: customer data doesn't belong mixed in with order data.
Second and Third Normal Form: Split Out Repeating Data
2NF and 3NF both boil down to the same practical idea: a column shouldn't depend on anything other than the table's key. customer_name and customer_city don't actually depend on the order — they depend on the customer. So they belong in their own table:
-- customers
customer_id | name | city
1 | Alice Smith | Chicago
2 | Bob Jones | Austin
-- orders (normalized)
order_id | customer_id | product | price
1 | 1 | Keyboard | 49.99
2 | 1 | Mouse | 19.99
3 | 2 | Monitor | 229.99
Now Alice's city exists in exactly one row. Change it once in customers, and every order that references her via the customer_id foreign key is automatically up to date — there's nothing left to go out of sync. Reconstructing the original view is just a matter of joining the two tables back together:
SELECT o.order_id, c.name, c.city, o.product, o.price
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;
When Denormalization Is the Right Call
Normalization isn't a rule to follow blindly all the way to the end. Highly normalized schemas require more joins to reconstruct a full picture, which can hurt read performance on large, read-heavy tables. It's common for real systems to deliberately denormalize — duplicating a bit of data on purpose — in specific places where read speed matters more than eliminating every last redundancy, such as caching a product's name directly on an order line so historical orders still display correctly even if the product is later renamed. The key word is deliberate: start normalized, and only denormalize a specific spot once you have a concrete reason to.
