<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Joins and combining tables
All lessons in this course

Lesson 23 of 54 16 minIntermediate

Designing tables: normalisation

Why one big table goes wrong, and how first, second and third normal form split it into tables where every fact is stored once. Plus when copying a value is correct, and when to denormalise on purpose.

You now know how to put tables back together with joins. This lesson is about the other half: deciding which tables to have in the first place. The rules are called normalisation, and their goal is simple: store every fact once.

What goes wrong in one big table

Here is an order sheet with the customer’s details on every line. Asha changes her email address, and the update is made for order 1 only:

Query.sql
CREATE TABLE dbo.OrderSheet (    OrderId   int           NOT NULL,    ProductId int           NOT NULL,    Customer  nvarchar(80)  NOT NULL,    Email     nvarchar(100) NULL,    Product   nvarchar(60)  NOT NULL,    Quantity  int           NOT NULL,    PRIMARY KEY (OrderId, ProductId));INSERT INTO dbo.OrderSheet VALUES    (1, 1, N'Asha Patel', N'asha@example.com', N'Claw hammer',       1),    (1, 3, N'Asha Patel', N'asha@example.com', N'Tape measure 8 m',  2),    (3, 2, N'Asha Patel', N'asha@example.com', N'Cordless drill',    1),    (3, 8, N'Asha Patel', N'asha@example.com', N'Wood screws (200)', 2); UPDATE dbo.OrderSheet SET Email = N'asha.p@example.com' WHERE OrderId = 1; SELECT DISTINCT Customer, Email FROM dbo.OrderSheet;
Output
Customer    Email
----------  ------------------
Asha Patel  asha.p@example.com
Asha Patel  asha@example.com

Asha now has two email addresses, and the database cannot tell you which is right. Repeated data causes three kinds of trouble, called anomalies:

AnomalyWhat happens
Updatea fact stored in many rows is changed in some of them, and the copies disagree
Insertyou cannot record a new customer or product until someone places an order
Deletedeleting a customer’s only order also deletes everything you knew about the customer

The normal forms

Interactive

From one sheet to third normal form

One sheet, lists in cells

OrderSheetkey: OrderId
OrderIdOrderDateCustomerEmailProductsQuantities
112024-02-03Asha Patelasha@example.comClaw hammer, Tape measure 8 m1, 2
222024-02-10Ben CarterNULLCement 25 kg, Sand 25 kg10, 10
332024-03-01Asha Patelasha@example.comCordless drill, Wood screws (200)1, 2
The Products and Quantities cells hold lists. You cannot join on them, index them or add them up without cutting text apart, and nothing keeps the two lists the same length. First normal form (1NF): one value per cell, no repeating groups, and a key that identifies each row.
FormRuleFix when broken
1NFone value per cell, no repeating groups (Product1, Product2, …), and a primary keyone row per item
2NF1NF, and every column depends on the whole key, not part of a composite keymove the columns to a table keyed by that part
3NF2NF, and no column depends on another non-key columnmove the dependent columns to a table of their own

A classic summary: every column must depend on the key, the whole key, and nothing but the key. In practice, ask of each column: “Is this a fact about what this table’s key identifies?” Email is a fact about a customer, not about an order line, so it belongs in Customers.

Putting it back together

Normalised tables do not lose the sheet: joins rebuild it whenever it is needed, from facts stored once:

Query.sql
SELECT o.OrderId, o.OrderDate, c.FirstName + N' ' + c.LastName AS Customer,       c.Email, p.Name AS Product, cat.Name AS Category, oi.QuantityFROM dbo.Orders AS oJOIN dbo.Customers  AS c   ON c.CustomerId   = o.CustomerIdJOIN dbo.OrderItems AS oi  ON oi.OrderId     = o.OrderIdJOIN dbo.Products   AS p   ON p.ProductId    = oi.ProductIdJOIN dbo.Categories AS cat ON cat.CategoryId = p.CategoryIdWHERE o.OrderId <= 3ORDER BY o.OrderId, p.ProductId;
Output
OrderId  OrderDate   Customer    Email             Product            Category  Quantity
-------  ----------  ----------  ----------------  -----------------  --------  --------
      1  2024-02-03  Asha Patel  asha@example.com  Claw hammer        Tools            1
      1  2024-02-03  Asha Patel  asha@example.com  Tape measure 8 m   Tools            2
      2  2024-02-10  Ben Carter  NULL              Cement 25 kg       Building        10
      2  2024-02-10  Ben Carter  NULL              Sand 25 kg         Building        10
      3  2024-03-01  Asha Patel  asha@example.com  Cordless drill     Tools            1
      3  2024-03-01  Asha Patel  asha@example.com  Wood screws (200)  Fixings          2

Now the email change is one row in Customers, and every order sees it at once:

Query.sql
UPDATE dbo.Customers SET Email = N'asha.p@example.com' WHERE CustomerId = 1; SELECT o.OrderId, c.FirstName, c.EmailFROM dbo.Orders AS oJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdWHERE c.CustomerId = 1;
Output
OrderId  FirstName  Email
-------  ---------  ------------------
      1  Asha       asha.p@example.com
      3  Asha       asha.p@example.com

Not every copy is a duplicate

OrderItems stores a UnitPrice, although Products has a Price. That is not a 3NF mistake, because they are different facts: the price paid on that day, and the price today. Cement was sold at a discount on order 7, and if the price rises tomorrow, old orders must keep the price they were charged:

Query.sql
SELECT oi.OrderId, o.OrderDate, p.Name, oi.UnitPrice AS PricePaid, p.Price AS PriceTodayFROM dbo.OrderItems AS oiJOIN dbo.Orders   AS o ON o.OrderId   = oi.OrderIdJOIN dbo.Products AS p ON p.ProductId = oi.ProductIdWHERE p.ProductId = 4;
Output
OrderId  OrderDate   Name          PricePaid  PriceToday
-------  ----------  ------------  ---------  ----------
      2  2024-02-10  Cement 25 kg       7.20        7.20
      7  2024-05-05  Cement 25 kg       7.00        7.20

The same goes for a delivery address copied onto an order: the customer may move later, but the parcel went to the old address. Ask whether the value can legitimately differ from the original. If it can, it is a separate fact and it is fine to store it.

Designing a many-to-many relationship

BuildMart wants to record suppliers. A product can come from several suppliers and a supplier sells many products, so this is many-to-many and needs a junction table. The cost price depends on the pair (the same product costs different amounts from different suppliers), so it lives there:

Query.sql
CREATE TABLE dbo.Suppliers (    SupplierId int          NOT NULL IDENTITY PRIMARY KEY,    Name       nvarchar(60) NOT NULL,    Phone      varchar(20)  NULL); CREATE TABLE dbo.ProductSuppliers (    ProductId  int           NOT NULL REFERENCES dbo.Products (ProductId),    SupplierId int           NOT NULL REFERENCES dbo.Suppliers (SupplierId),    CostPrice  decimal(10,2) NOT NULL,    PRIMARY KEY (ProductId, SupplierId));

Denormalising on purpose

Normalised tables are the right default for data that changes (OLTP systems such as a shop). Sometimes you copy data deliberately:

  • Reporting tables and data warehouses are read far more often than they are written, so they store pre-joined, repeated data to make reports fast.
  • Snapshots such as UnitPrice above, which record a value as it was.
  • Totals kept for speed, such as an order total stored next to its lines, but only with a reliable way (a trigger or a single code path) to keep it in step.
Quick check

OrderItems(OrderId, ProductId, Quantity, ProductName) has the key (OrderId, ProductId). Which normal form does ProductName break?

Quick check

Deliveries(DeliveryId, OrderId, DriverId, DriverPhone) has the key DeliveryId. What is wrong?

Practice

A delivery firm keeps one table: Deliveries(DeliveryId, DeliveryDate, DriverName, DriverPhone, VanReg, VanCapacityKg). Each driver can drive any van. Redesign it in third normal form with CREATE TABLE statements, using surrogate keys and foreign keys.

Show a hint

Driver details depend on the driver, and van details on the van. Make Drivers and Vans tables, and give Deliveries a DriverId and a VanId. Keep VanReg unique.

Show the solution
Solution.sql
CREATE TABLE dbo.Drivers (    DriverId int          NOT NULL IDENTITY PRIMARY KEY,    Name     nvarchar(60) NOT NULL,    Phone    varchar(20)  NULL); CREATE TABLE dbo.Vans (    VanId      int        NOT NULL IDENTITY PRIMARY KEY,    Reg        varchar(8) NOT NULL UNIQUE,    CapacityKg int        NOT NULL); CREATE TABLE dbo.Deliveries (    DeliveryId   int  NOT NULL IDENTITY PRIMARY KEY,    DeliveryDate date NOT NULL,    DriverId     int  NOT NULL REFERENCES dbo.Drivers (DriverId),    VanId        int  NOT NULL REFERENCES dbo.Vans (VanId));

Remember

  • Normalisation stores every fact once, so it cannot disagree with itself.
  • Repeated data causes update, insert and delete anomalies.
  • 1NF: one value per cell and a key. 2NF: depend on the whole key. 3NF: depend on nothing but the key.
  • Joins rebuild the combined view whenever you need it.
  • A copy that can legitimately differ, such as the price paid, is a separate fact, not a duplicate.
  • Denormalise deliberately, for a measured reason, and keep the copies in step.
0 of 54 lessons completed