Expert: design, speed and engines · Schema design and normalisation · topic 43 of 62 · 40 min
Normalisation: 1NF, 2NF, 3NF and BCNF
Explanation
The problem: one big sheet
Many businesses start with one wide sheet, one row per order line, with everything copied in. It is easy to read, so people ask "why not keep it like that?" The answer shows up as soon as the data changes.
order_id | order_date | customer | customer_phone | city | product | category | unit_price | qty
101 | 2025-01-05 | Asha Verma | 9876500001,9876500002 | Pune | Basmati Rice | Grocery | 650 | 2
101 | 2025-01-05 | Asha Verma | 9876500001,9876500002 | Pune | Toor Dal | Grocery | 160 | 3
103 | 2025-02-14 | Asha Verma | 9876500001,9876500002 | Pune | Cotton Kurta | Fashion | 899 | 2Three anomalies
An anomaly is a way the data can become wrong or impossible to store because of the table's shape.
- Update anomaly: Asha moves to Mumbai. Her city sits on 3 rows (or 3,000). Miss one and the database now says two different things.
- Insert anomaly: the shop adds a new product, the Power Bank, before anyone orders it. There is no order line to put it on, so the product cannot be recorded at all.
- Delete anomaly: the only order containing Cotton Kurta is deleted, and the fact that Cotton Kurta costs ₹899 and is Fashion disappears with it.
Functional dependency, in words
A functional dependency A → B ("A determines B") means: if you know A, there is exactly one B. Knowing a customer's id tells you her name; knowing a product tells you its category. In the sheet, order_id → order_date and customer, product → category and unit_price, and only the pair (order_id, product) → qty. Normalisation is simply making sure every column depends on the key of its own table.
First normal form (1NF): one value per cell
A table is in 1NF when every cell holds a single, atomic value, there are no repeating groups of columns, and rows have a key. The phone cell "9876500001,9876500002" breaks it: you cannot easily search, join or validate one number inside a list. Columns like phone1, phone2, phone3 break it too, in a sneakier way: a customer with four phones does not fit, and "find this phone" needs three ORs.
CREATE TABLE customer_phones (
customer_id INTEGER NOT NULL REFERENCES customers(id),
phone TEXT NOT NULL,
PRIMARY KEY (customer_id, phone)
);Second normal form (2NF): the whole key
2NF matters when the key has more than one column. A table is in 2NF when it is in 1NF and no non-key column depends on only part of the key. In the sheet the key is (order_id, product). But order_date depends on order_id alone, and category depends on product alone. These are partial dependencies. The fix is to move them out: order details go to orders, product details to products, and the line table keeps only what needs the whole pair, qty and the price paid.
Third normal form (3NF): nothing but the key
A table is in 3NF when it is in 2NF and no non-key column depends on another non-key column. After the 2NF split, orders still holds customer, phone and city. city depends on the customer, not on the order: order_id → customer → city. That chain is a transitive dependency. The fix is a customers table, with orders keeping only customer_id.
BCNF, briefly
Boyce-Codd normal form is a slightly stricter 3NF: for every dependency A → B, A must be a candidate key. It only bites in unusual tables with overlapping candidate keys. Classic case: tutoring(student, subject, teacher), where each teacher teaches one subject. The key is (student, subject), but teacher → subject and teacher is not a key. The BCNF fix is teachers(teacher, subject) plus tutoring(student, teacher). There are higher forms (4NF, 5NF), but 3NF or BCNF is the everyday goal.
The result, and getting the report back
The normalised version of the sheet is exactly the SHOP database you have used all course: customers, products, orders and order_items. The flat report is not lost; a JOIN rebuilds it whenever you need it.
SELECT o.id AS order_id, o.order_date, c.name AS customer, c.city, p.name AS product, oi.qty
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = 101;
-- 101 | 2025-01-05 | Asha Verma | Pune | Basmati Rice 5kg | 2
-- 101 | 2025-01-05 | Asha Verma | Pune | Toor Dal 1kg | 3Common mistakes
- Storing lists in one cell ("1,5,7" or JSON arrays) for things you search or join on. Fix: a child table with one row per item.
- Thinking order_items.price is a 3NF violation because products also has a price. It is not: it is the price paid at that moment, a different fact. Copy it on purpose.
- Splitting tables so far that every screen needs ten joins for no benefit. Fix: aim for 3NF, then denormalise only with a measured reason (next topic).
- Mixing up 2NF and 3NF. Fix: 2NF is about part of a composite key; 3NF is about one non-key column determining another.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Normalisation: 1NF, 2NF, 3NF and BCNF · 1/7
Normalisation means storing each fact in exactly one place. Let us see why that matters.
Prefer a video by a teacher? Find free videos on “database normalization 1nf 2nf 3nf bcnf explained with examples” on YouTube ↗
In real life: A school marks register moves off Excel
A school kept marks in a workbook: one row per student per exam, with the class teacher's name and phone copied onto every row, and subjects as columns (maths, science, english). When a teacher changed her number, hundreds of cells were out of date, and adding a new subject meant a new column everywhere. The normalised design has students, teachers, classes, subjects, exams and a marks table (student_id, exam_id, subject_id, marks). A new subject is now one new row, and the old report card is one JOIN away.
CREATE TABLE marks (
student_id INTEGER NOT NULL REFERENCES students(id),
exam_id INTEGER NOT NULL REFERENCES exams(id),
subject_id INTEGER NOT NULL REFERENCES subjects(id),
marks INTEGER CHECK (marks BETWEEN 0 AND 100),
PRIMARY KEY (student_id, exam_id, subject_id)
);Practice (0/4 done)
SHOP is the normalised version of a flat orders sheet. Rebuild the flat report for Delivered orders only. Return order_id, order_date, customer (the customer's name), city, product (the product's name), qty and price (from order_items), sorted by order_id and then by product.
customers_raw breaks 1NF: its phones column holds a comma-separated list such as '9876500001,9876500002'. Split it into one row per phone. Return customer_id and phone (row order does not matter). Use a recursive CTE, or SQLite's json_each.
enrolments(student_id, course_id, student_name, grade) has the primary key (student_id, course_id). Which normal form does it break, and why?
employees(id, name, dept_id, dept_city) has the primary key id. Each department is in one city. What is wrong, and what is the fix?
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Denormalisation, OLTP and OLAPSchema design and normalisation · same module
- Data types, naming and good habitsSchema design and normalisation · same module
- Normalisation and table designBackend Development · SQL in depth
- Theory questions you will be askedInterview questions and practice projects
- Joins: combining tablesBackend Development · SQL in depth
- Keys and relationshipsDatabases and the relational model
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
A product's category is copied onto every order line. You rename the category on some lines but not others. What is this called?
Answer: An update anomaly
One fact stored in many places can be changed in some places and not others.
Which table breaks first normal form?
Answer: customers(id, name, phone1, phone2, phone3)
phone1, phone2, phone3 is a repeating group. Use a child table with one row per phone.
What does the functional dependency customer_id → city mean?
Answer: Knowing customer_id tells you exactly one city
A → B means one A value determines one B value.
order_items.price repeats the price from products. Is that a normalisation mistake?
Answer: No: it records the price paid at the time, which is a different fact from today's price
Today's price and the price on an old bill are different facts. Copying it on purpose keeps old bills correct.
How is BCNF stricter than 3NF?
Answer: Every determinant (the A in A → B) must be a candidate key
BCNF closes a gap in 3NF that appears with overlapping candidate keys.
Which design breaks first normal form (1NF)?Related
Answer: A column phones containing "98100 11111, 98100 22222"
1NF needs one value per cell. A list of phones belongs in its own table with one phone per row.
A weekly sales report over five years of data, run by the business team, is typical of...Related
To get each customer's product categories in SHOP, which tables must the query pass through?Related
Answer: customers, orders, order_items and products
Keys connect customers → orders → order_items → products; you cannot skip a link.
Asha has three orders. In orders JOIN customers, how many result rows mention her?Related
Answer: Three, one per matching order
A join returns one row per matching pair, not one per key.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Schema design and normalisation” before its exam: Denormalisation, OLTP and OLAP, Data types, naming and good habits, Design project: an online store.
- Module 9 exam: Schema design and normalisation · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Indexes, query plans and views. 62 topics left in the course before the final exam.