Database Normalization

Design tables that avoid repeated data and update mistakes, using first, second and third normal form, and know when to denormalise.

Lesson 18 of 102Advanced17 min read

Many databases start life as a spreadsheet: one wide sheet where every row repeats the customer's name and city, the product's name and price, and anything else that seemed useful. It works for a while. Then a customer moves city and you have to change it in forty rows. You miss one, and now the database says she lives in two places at once.

Normalization is a set of rules for organising tables so each fact is stored in exactly one place. Data that belongs together lives in its own table, and tables are connected with keys. The rules are called normal forms, and the first three, 1NF, 2NF and 3NF, cover what almost every application needs.

In this lesson you'll see the problems repeated data causes, apply the three normal forms to a messy order sheet step by step, and learn when it's reasonable to break the rules on purpose.

The problem: repeated data

Here's a flat "orders sheet", the kind you'd get from a spreadsheet export:

order_idcustomercityproductstotal
1AnaLisbonBackpack 34.9934.99
3AnaLisbonPen 1.20 x1012.00
2BenLeedsNotebook 3.50 x517.50

Three kinds of problem, called anomalies, appear:

  • Update anomaly: Ana's city is stored in many rows. Change one and miss another, and the data contradicts itself.
  • Insert anomaly: you can't record a new product until someone orders it, because products only exist inside orders.
  • Delete anomaly: delete Ben's only order and you lose the fact that Ben exists at all.

First normal form (1NF)

A table is in 1NF when:

  1. Every column holds a single value: no lists like "Pen, Notebook" and no packed values like "Pen 1.20 x10".
  2. There are no repeating groups of columns such as product1, product2, product3.
  3. Each row can be identified by a primary key.

To fix the sheet, give each product in an order its own row with separate product, price and quantity columns. Single values can be searched, sorted, totalled and checked with constraints; packed text can't.

Second and third normal form

2NF applies when a table's key has more than one column. Every other column must depend on the whole key, not just part of it. In an order_lines table keyed by (order_id, product_id), quantity depends on both, but product_name and price depend only on product_id, so they move to a products table.

3NF says non-key columns must depend on the key directly, not through another column. In an orders table, city depends on the customer, not on the order, so it moves to customers, and the order keeps just customer_id.

A popular summary: every non-key column must depend on "the key, the whole key, and nothing but the key".

The normalised design

Applying the three forms to the sheet gives four tables:

  • customers (id, name, city)
  • products (id, name, price)
  • orders (id, customer_id, order_date)
  • order_lines (order_id, product_id, quantity, unit_price)

Each fact now lives in one place. Ana's city is stored once. A product can exist before anyone orders it. Deleting an order leaves the customer untouched. And whenever you want the flat view back for a report, a query with joins, saved as a view if you like, rebuilds it on demand.

Normalization Syntax

Normal form checklist

1NF  one value per cell, no column1/column2/column3, has a primary key
2NF  1NF + no column depends on only part of a composite key
3NF  2NF + no column depends on another non-key column
Ask of every column: does it describe the key, the whole key,
and nothing but the key?

Normalization Summary

  • Normalization stores each fact in one place to prevent update, insert and delete anomalies.
  • 1NF: single values per column, no repeating groups, a primary key.
  • 2NF: every non-key column depends on the whole of a composite key.
  • 3NF: non-key columns depend only on the key, not on other non-key columns.
  • Joins and views rebuild flat reports; denormalise deliberately and only for a measured reason.

Normalization Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Rebuilding the flat view with joins

SQL
SELECT o.id   AS order_id,
       c.name AS customer,
       c.city,
       p.name AS product,
       p.price,
       o.quantity
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products  p ON p.id = o.product_id
ORDER BY o.id;
Output
order_id | customer | city   | product      | price | quantity
---------+----------+--------+--------------+-------+---------
1        | Ana      | Lisbon | Backpack     | 34.99 | 1
2        | Ben      | Leeds  | Notebook     | 3.5   | 5
3        | Ana      | Lisbon | Pen          | 1.2   | 10
4        | Dev      | Leeds  | Desk lamp    | 24.5  | 1
5        | Chloe    | Lyon   | Water bottle | 12    | 2
6        | Ben      | Leeds  | Water bottle | 12    | 1
Line-by-line explanation
  1. Lines 2 to 3Ana's name and city appear in two rows of the result, but they're stored only once, in customers.
  2. Lines 8 to 9The joins put the pieces back together whenever a report needs them.
Example 2

Fixing an update anomaly

SQL
-- flat sheet: city repeated in every order row
UPDATE order_sheet SET city = 'Porto' WHERE customer = 'Ana';

-- normalised: one row to change
UPDATE customers SET city = 'Porto' WHERE id = 1;
Output
order_sheet: 2 rows updated (and any typo like 'Ana ' is missed)
customers:   1 row updated, every order sees the new city
Line-by-line explanation
  1. Line 2In the flat design, every copy must be found and changed, and any copy you miss creates contradictory data.
  2. Line 5In the normalised design, the city is a single fact about one customer.
Example 3

Splitting a repeating group

SQL
-- before (not 1NF)
CREATE TABLE courses_flat (
  student TEXT, course1 TEXT, course2 TEXT, course3 TEXT
);

-- after (1NF and beyond)
CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE courses  (id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE TABLE enrolments (
  student_id INTEGER REFERENCES students(id),
  course_id  INTEGER REFERENCES courses(id),
  PRIMARY KEY (student_id, course_id)
);
Output
Before: a fourth course needs a new column, and "who takes SQL?"
means searching three columns.
After: any number of courses per student, and one simple join
answers "who takes SQL?".
Line-by-line explanation
  1. Lines 2 to 4course1, course2 and course3 are a repeating group.
  2. Lines 7 to 8Students and courses each get their own table.
  3. Lines 9 to 13A junction table records which student takes which course, as in the relationships lesson.

Common Normalization Mistakes

Packing several values into one column

Don't
INSERT INTO orders (id, products) VALUES (7, 'Pen, Notebook, Pen');
Do
INSERT INTO order_lines (order_id, product_id, quantity) VALUES (7, 2, 2), (7, 1, 1);

Storing values you can calculate

Don't
CREATE TABLE order_lines (quantity INT, unit_price DECIMAL, line_total DECIMAL);
-- line_total can disagree with quantity * unit_price
Do
CREATE TABLE order_lines (quantity INT, unit_price DECIMAL);
-- calculate quantity * unit_price in queries or a view

Splitting tables with no reason

Don't
CREATE TABLE customer_names (customer_id INT, name TEXT);
CREATE TABLE customer_cities (customer_id INT, city TEXT);
Do
CREATE TABLE customers (id INT PRIMARY KEY, name TEXT, city TEXT);

Normalization is about removing repeated facts, not about making as many tables as possible.

Normalization in Real Projects

Normalised and denormalised designs

The databases behind shops, banks and booking systems are normalised, because correctness matters more than anything when data is constantly changing. Data warehouses used for analysis often denormalise on purpose, storing wide tables that are quick to read but rebuilt from the normalised source each night. The skill is knowing which you're building and why.

Normalization FAQ

Do I always need to normalise to third normal form?

For tables that store business data, such as customers, products and orders, aiming for third normal form is a sensible default because it prevents update mistakes. Reporting tables and caches are sometimes deliberately denormalised for speed, but that should be a documented decision, not an accident.

What is denormalisation?

Deliberately storing some data more than once, such as a customer's name on each order row or a running order total, to make particular reads faster or simpler. It trades extra storage and the work of keeping copies in step for fewer joins. Use it for measured performance needs.

Does normalisation make queries slower?

It means more joins, but databases are built to join well, especially on indexed keys. For most applications the difference is small, and normalised tables often make writes faster and data smaller. Measure before denormalising.

What is Boyce-Codd normal form?

BCNF is a slightly stricter version of third normal form: every column that determines other columns must be a candidate key. Most tables in third normal form already meet it. The normal forms and BCNF lesson covers the cases where they differ.

Is storing JSON in a column bad design?

Not always. JSON columns, such as PostgreSQL's jsonb, suit flexible extra attributes that you rarely filter or join on. Data you search, join or validate regularly, such as prices or customer ids, belongs in proper columns with types and constraints.

Normalization Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the rule of normalization.

Each fact is stored in exactly  place.
Multiple choiceeasy

Which column breaks first normal form?

Multiple choicemedium

You delete a customer's only order and lose all record of the customer. Which problem is this?

Multiple choicemedium

An orders table has columns id, customer_id and customer_city. Which normal form does customer_city break?

Multiple choicehard

Why does order_lines store unit_price even though products has a price?

Normalization Quiz

5 questions to check what you learned.

  1. What is the main goal of normalization?

    • Faster typing
    • Removing repeated data so each fact is stored once
    • Making tables wider
    • Using fewer keys
  2. What does 2NF add to 1NF?

    • Every table needs an index
    • No NULLs are allowed
    • Non-key columns must depend on the whole composite key
    • Columns must be sorted
  3. What's a repeating group?

    • Columns like item1, item2, item3 that hold the same kind of value
    • A GROUP BY clause
    • Two identical rows
    • A junction table
  4. How do you get a flat report from a normalised database?

    • Copy the data into a spreadsheet
    • Denormalise every table
    • Use UNION
    • Join the tables in a query, optionally saved as a view
  5. When can denormalisation make sense?

    • For read-heavy reporting where speed is measured and data is rebuilt from a normalised source
    • Always, it's simpler
    • When you don't know SQL
    • Never