Why normalization is worth 45 minutes
Session b1 ended with a flat export and a redundancy count. That redundancy is not an aesthetic problem - it is three specific, nameable failures waiting to happen, and every one of them will happen to a table that stores the same fact twice. Normalization is the process of removing them. The definitions (1NF, 2NF, 3NF) are just labels for how far along you are.
The three anomalies 7 min live
All three come from the same root cause: a fact stored in more than one place, or a fact that has nowhere of its own to live. Learn the shapes and you can spot them in a schema in seconds.
Live1NF, 2NF, 3NF - what each one actually forbids4 min▶
Each normal form removes one class of dependency. The plain-English version is short enough to hold in your head:
- 1NF - one value per cell. No comma-separated lists, no repeating groups (
product_1,product_2,product_3), no arrays pretending to be strings. If you have tosplit()a column to query it, you are not in 1NF. - 2NF - no partial dependency on a composite key. If the key is (order_id, product_id), then anything that depends on only part of that key belongs elsewhere. Order date depends on order_id alone, so it belongs on the order, not the line.
- 3NF - no transitive dependency. A non-key column must not depend on another non-key column. Product category depends on product name, not on the order line - so it belongs in a products table.
The shorthand every practitioner ends up with: every fact depends on the key, the whole key, and nothing but the key. The lab below is the same idea with the definitions removed and the failures left in.
Self-studyWhen storing a fact twice is correct3 min read▶
Bazaar's order_items table stores unit_price even though products already has list_price, and it stores merchant_id even though products already knows which merchant owns each product. Both look like textbook 3NF violations. Neither is a mistake.
- Point-in-time capture, not redundancy.
unit_priceis the price actually charged on that line, on that day. The product's list price will change; what you charged in April must not. These are two different facts that share a word - exactly the case session b1 flagged. - The same logic covers merchant_id: a product can be transferred between merchants, and the sale still belongs to whoever sold it at the time.
- How to tell the difference: ask whether the two copies are ever allowed to differ. If they must always agree, it is redundancy - normalize it. If they can legitimately differ over time, they are different facts - store both, and name them so the difference is visible (
list_pricevsunit_price, notpriceandprice).
This is why the lab below does not flag those two columns. Normalization is a tool for removing accidental duplication, not a rule that forbids deliberate history.
Normalize until the anomalies die ★ real SQL, measured
One wide table of 12 order lines - the flat export from session b1, shrunk so you can read it. Four levers. Each lever rebuilds the schema in real SQLite, and the lab then runs the actual failing statements against it: an UPDATE that changes one product's price, an INSERT of a product nobody has ordered, a DELETE of an order, and an INSERT with a bogus product id. The scorecard is measured, not scripted.
Start with everything off. Read the scorecard: five red cards. Note the redundancy count - that is how many repeated strings are stored right now.
Turn on one lever at a time. Watch which specific card goes green. Extract products and the update anomaly dies; extract customers and the redundancy count drops.
Open "Show the schema" after each change. That SQL is what your lever choices just built - the DDL is the lesson, not a by-product.
Finish with keys. The last lever is the one that makes the database refuse bad data instead of storing it. Watch the integrity card flip to "rejected".
Why the last lever matters most. Teams routinely split their tables and then never declare the foreign keys, because "the application handles it". The application handles it until a backfill script, a manual fix, or a second application writes to the same database. A constraint you declared but never enabled is a comment; SQLite in particular only enforces foreign keys after PRAGMA foreign_keys = ON, which is why the lab sets it explicitly. Session b4 makes this its main event.
Your turn: run the anomaly by hand ★ 8 min · build your own
The lab measures for you. Now cause the failure yourself, on Bazaar's real source tables, so the mechanism is unmistakable.
LiveQ1 · Cause an update anomaly, then detect it4 min▶
Bazaar's real schema is already normalized, so we recreate the flat shape first, then break it. Each run starts from a fresh database, so this is safe.
CREATE TABLE wide AS
SELECT oi.order_id, oi.line_no, p.product_name, p.category, oi.unit_price
FROM order_items oi JOIN products p ON p.product_id = oi.product_id;
-- a well-meaning price correction that only finds one row
UPDATE wide SET unit_price = 149.00
WHERE product_name = 'Studio Headphones' AND order_id = (
SELECT MIN(order_id) FROM wide WHERE product_name = 'Studio Headphones');
SELECT product_name,
COUNT(DISTINCT unit_price) AS distinct_prices_stored,
COUNT(*) AS rows_storing_a_price
FROM wide
WHERE product_name = 'Studio Headphones'
GROUP BY product_name;
Two distinct prices for one product, and nothing in the database says which is correct. In the normalized schema this update touches exactly one row in products and the question cannot arise.
LiveQ2 · Prove the normalized schema cannot have the problem4 min▶
UPDATE products SET list_price = 149.00 WHERE product_name = 'Studio Headphones';
SELECT product_name, COUNT(*) AS rows_storing_a_price,
COUNT(DISTINCT list_price) AS distinct_prices_stored
FROM products
WHERE product_name = 'Studio Headphones'
GROUP BY product_name;
One row, one price. That is the entire benefit of normalization stated as a query result.
Self-studyQ3 · Find a 1NF violation in the wild3 min▶
No SQL. Bazaar's source schema has no comma-separated columns, because it was designed rather than accreted. Almost every real schema has at least one: a tags column holding "sale,clearance,final", a sizes column holding "S/M/L", an address column holding a whole postal address.
Find one in a schema you work with. Then write the query someone would need to answer "how many products are tagged clearance" against it, and notice that it involves LIKE '%clearance%' - which also matches "not-clearance". That is what a 1NF violation costs: every query about that column becomes a string-matching guess.
Try it yourself - this week ◐ 20-30 min total
- Run the lab once with every lever off and once with all four on. Screenshot both scorecards side by side - that pair is the most persuasive normalization argument you will ever show a stakeholder.
- Take one wide table you own. For each of the three anomalies, write the exact statement that would cause it. If you can write all three, the table needs splitting.
- Find a column in your own schema that stores a fact you could look up somewhere else. Decide, in writing, whether it is redundancy or point-in-time capture. Both answers are fine; not knowing is not.
- Check whether the foreign keys in your database are declared, enabled, and actually enforced. In SQLite, run
PRAGMA foreign_keys;- if it returns 0, none of them are doing anything. - Bring one many-to-many relationship from your work to session b3 - resolving those is what b3 is about.
Sources covered
Full source map in materials/official-course-map.md. This page covers:
PRAGMA foreign_keysLab lever 4 · declared vs enforced constraintsThree questions before you go 🎯 ◐ 90 seconds
1 · A wide table stores a product's category on every order line. Which anomaly does that expose you to?
The three anomalies share one root cause - a fact stored in more than one place, with nowhere of its own to live. That is why extracting products into its own table kills all three at once rather than one at a time.
2 · What does 3NF forbid that 2NF allows?
2NF removes partial dependencies on part of a composite key; 3NF removes transitive dependencies between non-key columns - category depending on product name rather than on the row's key. Composite keys are fine in both, and neither has anything to say about NULLs.
3 · Bazaar's order_items stores unit_price even though products has list_price. Why is that not a normalization error?
The test is whether the two copies are ever legitimately allowed to differ. Must always agree means redundancy - normalize it. Can differ over time means two distinct facts - store both, and name them so the difference is visible.