Modules/Database/ Normalization Lab

Normalization: 1NF, 2NF, 3NF

One messy table, split one rule at a time. Each split removes a specific kind of redundancy — and a specific kind of bug.

Stage

raw 1NF 2NF 3NF

The raw table: one row per order, a comma-separated list of products.

The Table(s) At This Stage

Redundancy

Redundant Cells
4
Tables1

 

Normalization: A Practical Guide

Splitting a table so each fact is stored exactly once.

Start here

A denormalized table stores the same fact in more than one place, which means updating it can leave two copies disagreeing. Normalization is a sequence of rules, each one removing a specific kind of redundancy by moving data into its own table.

The three rules

  • 1NF — atomic values. No column holds a list. "Pen, Notebook" in one cell becomes two rows.
  • 2NF — no partial dependency. With a composite key, every other column must depend on the whole key, not just part of it. Customer name depends only on order_id, not on (order_id, product) — so it moves out.
  • 3NF — no transitive dependency. A column cannot depend on another non-key column. Customer city depends on customer, not directly on the order — so it moves to its own table.

The problem normalisation solves

Put everything in one wide table and three specific problems appear. They have names, and recognising them is most of the skill.

Start with a table that stores an order, its customer and its product all in one row:

OrderIDCustomerCustomerCityProductPrice
1AliceLondonKeyboard45
2AliceLondonMouse20
3BobLeedsKeyboard45
  • Update anomaly. Alice moves to Bristol. Her city is stored in two rows, and if you update only one, the database now holds two contradictory answers to the same question.
  • Insert anomaly. You cannot record a new customer until they place an order, because the row has no order to hang on.
  • Delete anomaly. Delete order 3 and you lose the only record that Bob exists, and the only record of the keyboard's price if it was the last one.

Splitting the data into Customers, Products and Orders makes each fact live in exactly one place. Update Alice's city once and everything that references her sees the change, because there is nothing else to update.

The three normal forms, with a test each

First normal form (1NF): one value per cell. No comma-separated lists, no repeating column groups like phone1, phone2, phone3.

Test: does any cell contain more than one thing? A tags column holding "urgent,billing,uk" fails. The fix is a separate row per tag.

Second normal form (2NF): no partial dependencies. Applies when the primary key is made of several columns. Every non-key column must depend on the whole key, not just part of it.

Test: with a key of (OrderID, ProductID), does ProductName depend on both? No — it depends on ProductID alone. It belongs in a Products table.

Third normal form (3NF): no transitive dependencies. Non-key columns must not depend on other non-key columns.

Test: does CustomerCity depend on the order, or on the customer? On the customer — so it belongs in Customers, keyed by CustomerID.

The informal summary that people memorise, and which is genuinely accurate: every non-key column depends on the key, the whole key, and nothing but the key.

Higher forms exist — BCNF, 4NF, 5NF — and address rarer situations with overlapping candidate keys and multi-valued dependencies. In practice, 3NF is where most well-designed transactional schemas sit.

When to denormalise deliberately

Normalisation optimises for correctness of writes. Reads pay for it in joins, and past a certain scale that cost becomes the dominant one.

Denormalising means storing something redundantly on purpose, accepting the duplication in exchange for speed. It is a legitimate engineering decision when:

  • A report joins eight tables and runs constantly.
  • The redundant data does not change — the price at the time of the order genuinely belongs on the order, because it must not change when the product's price does.
  • You are building an analytics warehouse, where star schemas are deliberately denormalised for query speed.
  • A counter (comment_count) is read thousands of times per write.

The cost is that you now own the consistency problem the database was solving for you. Every denormalised copy needs a plan: a trigger, an application-level update, or a scheduled rebuild. Denormalising without that plan is not a trade-off, it is a bug waiting for its first inconsistency.

The order to work in: normalise first, measure, then denormalise the specific hot paths with evidence. Starting denormalised because it might be faster is how schemas become unmaintainable.

The anomalies, on a table that has them

Normalisation is usually taught as a list of numbered forms. It is easier to see as three specific bugs that a badly-shaped table makes possible, and this builds such a table so you can commit each one.

query.sqlSQLite
Result

Guided tour

  1. Start at raw. One table, one row per order, a multi-valued products column — not even 1NF.
  2. Normalize to 1NF. Products become atomic — one row per (order, product) — but now customer, city and price all repeat across the exploded rows.
  3. Normalize to 2NF. Orders and Products split out — price no longer repeats per line item. Redundant cells fall from 8 to 2.
  4. Normalize to 3NF. Customers splits out from Orders — city no longer repeats per order for the same customer. Redundant cells hit 0.
  5. Read what is left. order_id and customer still repeat across tables — that is a normal foreign key, not redundancy. Redundancy is a *fact* stored twice; a key linking two facts is the whole point of splitting them apart.

Where this goes wrong

Over-normalizing has a cost too: every extra table is another join at query time. Reporting and analytics workloads often deliberately denormalize back down for read speed, accepting the redundancy because the data is written once and read constantly. Normalize for correctness where writes happen; consider denormalizing where reads dominate.

Summing up

1NF removes multi-valued columns, 2NF removes columns that depend on only part of a composite key, and 3NF removes columns that depend on another non-key column rather than the key itself. Each step moves a fact into the one table where it is defined once, which is what makes update anomalies structurally impossible rather than merely unlikely.

A worked split

Take the wide table from earlier and normalise it properly.

Customers — one row per customer:

CREATE TABLE customers (
  id    INT PRIMARY KEY,
  name  VARCHAR(100) NOT NULL,
  city  VARCHAR(100)
);

Products — one row per product, with the current price:

CREATE TABLE products (
  id    INT PRIMARY KEY,
  name  VARCHAR(100) NOT NULL,
  price DECIMAL(10,2) NOT NULL
);

Orders — one row per order, referencing the customer:

CREATE TABLE orders (
  id          INT PRIMARY KEY,
  customer_id INT NOT NULL REFERENCES customers(id),
  ordered_at  TIMESTAMP NOT NULL DEFAULT NOW()
);

Order lines — one row per product on an order, with the price as charged:

CREATE TABLE order_lines (
  order_id     INT NOT NULL REFERENCES orders(id),
  product_id   INT NOT NULL REFERENCES products(id),
  quantity     INT NOT NULL CHECK (quantity > 0),
  unit_price   DECIMAL(10,2) NOT NULL,   -- deliberately duplicated
  PRIMARY KEY (order_id, product_id)
);

That unit_price looks like a normalisation violation and is not. The price on the order is a historical fact that must never change; the price in products is the current one. They are two different facts that happen to be equal today, which is exactly the distinction normalisation asks you to make.

Questions people ask

Do I need to go beyond 3NF? Rarely. BCNF matters when a table has overlapping candidate keys; most schemas never hit that case.

Is normalisation bad for performance? It costs joins on reads and saves work on writes, while keeping tables smaller and more cacheable. For transactional systems it is usually a net win; for analytics, denormalised star schemas generally win.

What about NoSQL? Document databases embrace denormalisation as the default — embed related data in one document and read it in one operation. The trade is the same one, made in the other direction, and the consistency work moves to your application.

How do I know if my schema is normalised? Take each non-key column and ask what it actually depends on. If the answer is anything other than "the whole primary key", it belongs elsewhere.

Should every table have a surrogate key? A single-column integer or UUID key is easier to reference and to change than a natural key, and most schemas use one. Keep a unique constraint on the natural key as well, so the real rule is still enforced.

Can I normalise an existing production table? Yes, incrementally: create the new tables, backfill, write to both for a period, migrate readers, then drop the old columns. Doing it in one step on a live system is how outages happen.

Recap in one screen

  • Normalisation removes redundancy so that each fact is stored once.
  • Redundancy causes update, insert and delete anomalies — the concrete reasons the rules exist.
  • 1NF: one value per cell. 2NF: depend on the whole key. 3NF: depend on nothing but the key.
  • Denormalise deliberately, for measured read paths, with a plan for keeping copies consistent.
  • Historical values, such as the price charged on an order, are not redundancy — they are different facts.

Keys, and why they carry the design

Normalisation is a statement about dependencies, and keys are how those dependencies get enforced.

Primary key — the column, or set of columns, that identifies a row uniquely. It is NOT NULL by definition and is the anchor everything else references.

Foreign key — a column that references another table's primary key. It is what stops an order pointing at a customer who does not exist, and what ON DELETE CASCADE or ON DELETE RESTRICT hangs off.

Candidate key — any column set that could serve as the primary key. An email address and a national insurance number might both be candidates; you pick one as primary and enforce the others with UNIQUE.

Composite key — a key made of several columns, common in the junction tables that model many-to-many relationships.

The junction table is the pattern to remember, because it is how normalisation handles relationships that a single foreign key cannot express. Students and courses are many-to-many, so neither table can hold a reference to the other; a third table with (student_id, course_id) as its primary key does the job, and any facts about the enrolment itself — the grade, the enrolment date — belong there too.

A last practical note: declare your foreign keys. Enforcing referential integrity in application code instead means the database will eventually contain rows the application says are impossible, usually inserted by a migration script at two in the morning.

Predict, then reveal

About to run: press Normalize →. Before it does — what happens to the readout?

Committing to an answer first is the point — the reveal runs the experiment on the visualisation above and reads the real value back, so nothing here is scripted.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. What does this module say about “Start here”?

  2. What does this module say about “The three rules”?

  3. What does this module say about “The problem normalisation solves”?

Cheat sheet

Normalization (1NF, 2NF, 3NF) in SQL

A denormalized table stores the same fact in more than one place, which means updating it can leave two copies disagreeing. Normalization is a sequence of rules, each one removing a specific kind of redundancy by moving data into its own table.

DATABASE · vizlearn.in/database/normalization_in_sql.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.