Normalisation & schema design
1NF, 2NF and 3NF with worked examples, modelling relationships, naming and when to denormalise.
Good SQL can't rescue a bad schema. Schema design means deciding which tables exist, which columns they have and how they relate. Normalisation is a step-by-step method for removing redundancy so that each fact is stored exactly once. That avoids the anomalies that slowly corrupt data.
Why redundancy hurts#
Imagine one big spreadsheet-style table of orders:
It looks convenient, but:
- Update anomaly: when Asha changes her email, you must update every one of her orders. Miss one and the data contradicts itself.
- Insert anomaly: you can't record a new customer until they place an order.
- Delete anomaly: delete Rohan's only order and you lose the fact that Rohan exists at all.
- Querying pain: "how many mice did we sell?" means string-searching the
productstext.
Normalisation fixes these problems one rule at a time.
First normal form (1NF): atomic values, no repeating groups#
Each column holds a single value, and each row is unique (has a key).
The products column holds a list, which breaks 1NF. So do repeating columns like product1, product2, product3. Move the repeating data into its own table, with one row per item:
Now "how many mice?" is a simple SUM(qty) ... WHERE product = 'Mouse'.
Second normal form (2NF): depend on the whole key#
1NF, plus every non-key column depends on the entire primary key, not just part of it.
2NF matters for tables with composite keys. Suppose order_lines has the key (order_id, product_id) and the columns qty, product_name and order_date:
qtydepends on both the order and the product. ✅product_namedepends only onproduct_id. ❌ It belongs inproducts.order_datedepends only onorder_id. ❌ It belongs inorders.
Move each partial dependency to the table where it fully belongs.
Third normal form (3NF): no transitive dependencies#
2NF, plus non-key columns depend only on the key, not on other non-key columns.
In orders(id, customer_id, customer_email, city), customer_email depends on customer_id, which in turn depends on the order id. That's a transitive dependency. The email is a fact about the customer, so it belongs in customers.
A memorable summary: every non-key column must provide a fact about the key, the whole key, and nothing but the key.
There are higher forms (BCNF, 4NF, 5NF) for rarer situations, but a schema in 3NF is the practical goal for most applications.
The normalised design#
The old spreadsheet is now one join away, and no fact is stored twice:
Is order_items.unit_price a 3NF violation, since products.price already exists? No. They're different facts: the price today versus the price the customer actually paid. Historical values belong to the transaction. Spotting the difference between duplication and history is a core design skill.
Modelling relationships#
Junction tables often carry their own data: quantity, enrolment date, role, grade.
Practical design guidelines#
- Every table gets a primary key. Usually a surrogate
id, plusUNIQUEconstraints on natural identifiers (email, SKU). - Consistent naming. Use lowercase
snake_case, plural table names (orders),idfor the key and<singular>_idfor foreign keys (customer_id). Avoid reserved words. - Use the right types and constraints (
DECIMALfor money,NOT NULLby default,CHECKfor ranges) and declare foreign keys. - Lookup tables vs ENUM. If a list of values changes or has attributes (statuses with labels and sort order), use a table.
- Audit columns.
created_atandupdated_aton most tables cost little and help debugging. - Soft deletes (
deleted_at DATETIME NULL) keep history but complicate every query. Use them deliberately. - Don't store what you can compute cheaply (order totals, ages) unless you have a measured performance reason.
- Avoid the EAV trap (
entity, attribute, valuetables for everything). It throws away types and constraints. For truly flexible attributes, aJSONcolumn is usually better.
When to denormalise#
Normalisation optimises for correct writes. Sometimes reads matter more. Common, deliberate denormalisations:
- A cached
orders.total, maintained by the application or a trigger, so order lists don't need to sum items. - Summary/reporting tables rebuilt nightly by a scheduled event.
- Copying a value for history (the
unit_priceabove).
Denormalise after you've measured a real performance problem, document why, and make sure something keeps the copies in sync.
A design checklist#
- List the things (entities): customers, products, orders.
- List each thing's facts (attributes) and pick its key.
- Draw the relationships and their cardinalities (1–1, 1–N, N–M).
- Check 1NF (no lists), 2NF (whole key) and 3NF (nothing but the key).
- Add types,
NOT NULL,UNIQUE,CHECKand foreign keys. - Write the queries your app needs against the design before building it, and add indexes for them (next lesson).
What's next#
A well-designed schema still needs the right indexes to stay fast as tables grow from hundreds of rows to hundreds of millions.
Check your understanding
Quick quiz
1.A column
phone_numbersholds values like '98200 11111, 98200 22222'. Which normal form does this break?2.In
order_items(order_id, product_id, qty, product_name)with key (order_id, product_id), why doesproduct_namebreak 2NF?3.How do you model a many-to-many relationship such as students ↔ courses?
Finished reading?
Mark this lesson complete to track your progress.