When does denormalization actually win?
It wins when a specific read is the bottleneck and you have measured that it is. The pattern that reliably works is to add a column or a summary table that serves one expensive query, rather than to denormalise a whole schema.
-- The normalised source of truth.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
city VARCHAR(80) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers (id),
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO customers (id, name, city) VALUES
(1, 'Ada Lovelace', 'London'),
(2, 'Grace Hopper', 'New York');
INSERT INTO orders (id, customer_id, amount) VALUES
(1, 1, 120.00),
(2, 1, 340.50),
(3, 2, 75.25);
-- The denormalised read model: the city is copied in.
CREATE TABLE orders_read_model (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
customer_name VARCHAR(120) NOT NULL,
customer_city VARCHAR(80) NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO orders_read_model (id, customer_id, customer_name, customer_city, amount)
SELECT o.id, o.customer_id, c.name, c.city, o.amount
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id;
SELECT * FROM orders_read_model ORDER BY id;Three rows come back, each with the customer name and city already on the order row. The read query no longer has to join anything.
Denormalisation wins in four recognisable situations. The read is far more frequent than the write, and the join is expensive — a feed, a search index, a report that runs every few minutes. The table is huge and the join is over billions of rows, so even a small per-row cost dominates. The read crosses a network or shard boundary, so assembling the answer requires a remote call per row and the duplicated copy makes it local. And the read has to be served from a cache anyway, in which case the shape of the cached row is what you should optimise for.
What it costs is consistency, and the cost depends entirely on how you keep the copy in step. Update it in the same transaction as the source and the write path gets slower but the copy is never stale. Update it asynchronously and the copy can lag by however long the job takes, so you have to decide whether that matters and how anyone finds out. Never update it and it becomes a snapshot, which is fine for a historical report and wrong for anything current.