1. Why Normalize: Update, Insert & Delete Anomalies
orders_bad
+----+---------------+------------------------+----------+-------+
| id | customer_name | customer_email | product | price |
+----+---------------+------------------------+----------+-------+
| 1 | Asha | asha@example.com | Mouse | 12.50 |
| 2 | Asha | asha@example.com | Keyboard | 39.99 |
+----+---------------+------------------------+----------+-------+
Three concrete problems this creates:
- Update anomaly — if Asha changes her email, both rows need updating; miss one and the database now contains two different "true" emails for the same person.
- Insert anomaly — Asha can't exist in the system until she places an order, because there's no row to put her in without a product and price.
- Delete anomaly — deleting Asha's only order deletes all record that she exists as a customer at all.
Every one of these traces back to the same root cause: customer facts and order facts are mixed into one table instead of separated — exactly what normalization gives a systematic process for fixing.
2. 1NF, 2NF & 3NF
Each normal form fixes one specific kind of redundancy, building on the one before it.
- 1NF (First Normal Form) — every column holds a single, atomic value; no repeating groups (a column holding a comma-separated list of products is a 1NF violation).
- 2NF — every non-key column depends on the entire primary key, not just part of it (only relevant for tables with a composite primary key).
- 3NF — no non-key column depends on another non-key column (a
customer_emailcolumn that depends oncustomer_namerather than directly on the order's own key is a 3NF violation — exactly the pattern in the bad table above).
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id),
product TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL
);
Asha's email now lives in exactly one place — updating it is one write, she can exist with zero orders, and deleting an order never touches her customer record at all. All three anomalies from the previous section are gone by construction.
3. Foreign Keys & Referential Integrity
A foreign key doesn't just document a relationship — Postgres actively enforces it, refusing operations that would leave a reference pointing at nothing.
INSERT INTO orders (customer_id, product, price) VALUES (999, 'Mouse', 12.50);
-- ERROR: insert or update on table "orders" violates foreign key constraint
-- customer_id 999 doesn't exist in customers
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
-- ...
);
-- deleting a customer now automatically deletes their orders too,
-- rather than leaving orphaned rows or failing with an error
ON DELETE CASCADE is a real design decision, not a default to reach
for automatically — ON DELETE RESTRICT (the implicit default, blocking
the delete entirely while references exist) is often the safer choice for data
you'd rather not lose silently as a side effect of an unrelated delete.
4. One-to-Many & Many-to-Many
One-to-many (one customer, many orders) is what every example this week has already modeled — a foreign key on the "many" side. Many-to-many (many students enrolled in many courses) needs a third table in between.
CREATE TABLE students (id SERIAL PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE courses (id SERIAL PRIMARY KEY, title TEXT NOT NULL);
CREATE TABLE enrollments (
student_id INT NOT NULL REFERENCES students(id),
course_id INT NOT NULL REFERENCES courses(id),
PRIMARY KEY (student_id, course_id) -- a composite key: prevents duplicate enrollment
);
This join table (or "junction table") is the standard pattern —
neither students nor courses references the other
directly; enrollments sits between them, and each row represents one
real enrollment. Its composite primary key (both columns together) is what
structurally prevents the same student from enrolling in the same course twice.
5. Hands-on Exercise
Redesign a denormalized schema
Take a deliberately bad table, identify its anomalies, and fix it with a normalized design.
Requirements:
- Given a single
enrollments_badtable with columnsstudent_name,student_email,course_title,instructor_name(create it and insert a few rows yourself, with at least one student enrolled in two courses), identify in writing (a comment) at least one update, insert and delete anomaly it has. - Redesign it into
students,courses, and anenrollmentsjoin table with a composite primary key. - Add a foreign key from
enrollmentsto bothstudentsandcourses, and confirm Postgres rejects an enrollment referencing a nonexistent student or course. - Add an
instructorstable and a foreign key fromcoursesto it, rather than storinginstructor_namedirectly on courses. - Write a query, using a join from Week 3, that reconstructs the original flat view (student name, course title, instructor name) from your normalized tables — proving no information was lost in the redesign.
If inserting a duplicate enrollment (the same student_id and course_id pair) doesn't get rejected, double check your composite primary key is actually declared as PRIMARY KEY (student_id, course_id) on both columns together — declaring two separate single-column primary keys isn't valid, and a missing composite key silently allows the exact duplicate-enrollment problem the join table is meant to prevent.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Using the bad orders_bad table as an example, why does changing a customer's email require finding and updating every one of their order rows, and what goes wrong if one is missed?
Using the bad orders_bad table as an example, why does changing a customer's email require finding and updating every one of their order rows, and what goes wrong if one is missed?
Because the email is repeated on every row that mentions that customer, rather than stored once — updating it means updating every copy. Missing even one leaves the database with two contradictory "true" emails for the same customer, with no way for a later reader to know which one is actually current — precisely the update anomaly normalization eliminates by storing each fact exactly once.
Q2
What specific redundancy does 3NF eliminate that 1NF alone does not catch?
What specific redundancy does 3NF eliminate that 1NF alone does not catch?
3NF eliminates a non-key column depending on another non-key column rather than on the table's own key — like customer_email depending on customer_name rather than directly on the order's own identity. 1NF only requires atomic, non-repeating values per column; a table can satisfy 1NF while still having this kind of redundant, anomaly-prone dependency between its columns.
Q3
Why does ON DELETE CASCADE deserve to be a deliberate decision rather than a default choice on every foreign key?
Why does ON DELETE CASCADE deserve to be a deliberate decision rather than a default choice on every foreign key?
It causes a delete on the referenced row to automatically and silently delete every row that references it — appropriate when the dependent rows have no meaning without their parent (order items without their order), but genuinely dangerous when the dependent data should survive independently or the delete should require explicit confirmation instead. Applying it everywhere by habit risks losing data nobody intended to delete.
Q4
Why does a many-to-many relationship need a separate join table, where a one-to-many relationship just needs a foreign key on one side?
Why does a many-to-many relationship need a separate join table, where a one-to-many relationship just needs a foreign key on one side?
A single foreign key column can only point to one row on the other side — perfect for "many orders, one customer each," but a student enrolled in multiple courses (and a course with multiple students) can't be expressed with a single foreign key in either direction. A join table's each row records one specific pairing, letting both sides have arbitrarily many relationships to the other.