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.
What goes wrong in one big table
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;Customer Email ---------- ------------------ Asha Patel asha.p@example.com Asha Patel asha@example.com
| Anomaly | What happens |
|---|---|
The normal forms
| Form | Rule | Fix when broken |
|---|---|---|
Putting it back together
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;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 2UPDATE 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;OrderId FirstName Email
------- --------- ------------------
1 Asha asha.p@example.com
3 Asha asha.p@example.comNot every copy is a duplicate
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;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.20Designing a many-to-many relationship
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
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.
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
Show the solution
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.