Topic 28

When to Bend the Rules

Design

Lora's monthly revenue report joins bookings to screenings to films, re-adding every ticket sold since the Marquee opened — and it runs every night, recomputing the same totals over and over. One day the developer building her website suggests something that should make your normalization instincts flinch: "why don't we keep a daily_sales table with the totals already added up?"

That suggestion has a name: denormalization — deliberately storing derived, duplicated data that a query could compute from the real tables. After the last two pages it sounds like heresy, and here is the honest adult version: it is neither sin nor superpower. It is a trade — speed and convenience, bought with exactly the risk Topic 26 named. The difference between a professional and a mess is whether the risk is managed or stumbled into.

The Legitimate Pressures

The pressures that justify the trade are real and specific. The first is the recomputing report: a query that re-aggregates millions of rows to produce numbers that were identical yesterday. Every night the Marquee's report re-adds January's tickets, which have not changed since January. Past a certain size, that waste stops being philosophical and starts being minutes of waiting.

The second is the read-heavy page. Suppose the Marquee's website shows each film's average rating from thousands of review rows, and the film page is visited constantly. Computing the average on every single visit means the same arithmetic thousands of times an hour, for a number that changes only when a review arrives.

The third is the hot JOIN: the same four-table query running a thousand times a minute. The JOIN is correct, the schema is right, and sheer repetition still makes it the most expensive sentence in the building. Notice what all three pressures have in common: they are measured read pain, not aesthetic complaints about queries being long.

The Price Tag Never Changes

Now the cost, and it should sound familiar. The moment daily_sales exists, one fact — Tuesday's revenue — lives in two places: implicitly in the bookings, explicitly in the summary. The copy can disagree with the source. Every write now has two jobs: record the booking and update the summary, and the day someone forgets the second job, the report lies.

Recognize it? That is the update anomaly from Topic 26, invited back in on purpose. Denormalization does not dodge the disease; it signs a waiver accepting the risk. Which is exactly why the decision deserves a page: you may only sign waivers for risks you can name and contain.

Managed Bending

A homely picture shows what containment looks like. Lora keeps the month's receipts in a shoebox, and a running total on the fridge whiteboard. The whiteboard is fast to glance at and wrong the moment someone forgets to update it — but that is fine, because the shoebox never lies. Any dispute, you recount the receipts and correct the board. The whiteboard is a convenience; the shoebox is the truth. Map that onto tables and drop it: the summary is the whiteboard, the normalized tables are the shoebox, and the shoebox must always win an argument.

Three disciplines turn a dangerous copy into a managed one. First, the normalized tables stay the source of truth — apps write bookings there, never directly into the summary. Second, the summary must be rebuildable: one query, run from scratch, regenerates it entirely. Third, refresh on a schedule, so any drift has a known maximum age. Here is the Marquee's summary with its rebuild:

The summary table — and the query that can always rebuild it
CREATE TABLE daily_sales (
  sales_date   DATE PRIMARY KEY,
  tickets_sold INTEGER NOT NULL,
  revenue      DECIMAL(10,2) NOT NULL
);

-- The rebuild: regenerate every row from the source of truth
INSERT INTO daily_sales (sales_date, tickets_sold, revenue)
SELECT CAST(s.starts_at AS DATE), COUNT(*), SUM(s.price)
FROM bookings b
JOIN screenings s ON b.screening_id = s.screening_id
GROUP BY CAST(s.starts_at AS DATE);

Read the second statement carefully, because it is the safety argument. Everything in daily_sales is derived by one GROUP BY over the real tables — nothing in the summary exists that the sources cannot regenerate. If the summary ever drifts, delete it and run the rebuild; the truth was never at risk. Engines even offer a built-in version of this pattern called a materialized view — a stored, refreshable query result — which you now have the vocabulary to recognize when you meet one.

The Decision Smell

So when do you bend? The test is uncomfortable but short: bend for measured read pain, never for "JOINs are annoying". A report that demonstrably takes minutes, a page that demonstrably hammers the same aggregate — those earn a rebuildable summary. A developer who would simply rather not type JOIN has a query-writing preference, not a design problem, and Chapter 8's indexes will usually solve real slowness before denormalization needs to.

Notice that the rule of thumb from Chapter 1 has survived the whole negotiation intact: relational and normalized by default, something else when a specific, named reason says so. Bending is a receipt-backed exception over a normalized core. A denormalized core — everything wide, nothing rebuildable — is not bold engineering; it is Chapter 1's spreadsheet with better marketing.

Should this become a summary table?
The nightly report re-adds millions of unchanged rows and takes minutes?Summary table, refreshed on a schedule
The same average is recomputed on every page visit, thousands per hour?Rebuildable copy for the hot path
The four-table JOIN just feels annoying to write?Keep the JOIN
The copy could not be regenerated from the source tables?Don't — that's a contradiction, not a cache
Common Confusions
  • "Denormalization is what fast teams do; normalization is academic." Fast teams denormalize specific, measured hot paths over a normalized core. A denormalized core is Chapter 1's spreadsheet wearing an engineering badge.
  • "Once denormalized, always denormalized." A rebuildable summary can be dropped or regenerated freely — that reversibility is the whole design. It is the un-rebuildable copy that traps you.
  • "This page contradicts the normalization page." Topic 26 taught the price; this page shows when it is worth paying. The default survives untouched — the summary is an exception with a receipt, not a new rule.
  • "The summary saves work, so it's free." It moves work: every write now has two jobs, and the risk of the copy drifting is permanent. The trade is real on both sides, which is why it needs measured pain to justify it.
Why It Matters
  • Real production schemas all contain some managed bending — summary tables, cached counts, materialized views. You can now tell discipline from rot at a glance, which is a genuinely senior instinct.
  • "Rebuildable from the source of truth" is the one-question test that separates a cache from a contradiction, and it applies far beyond databases.

Knowledge Check

Which of these is a legitimate reason to add a daily_sales summary table?

  • The four-table revenue JOIN is tedious to write out
  • The nightly report measurably takes minutes re-aggregating unchanged rows
  • Other successful companies all keep summary tables, so it must be best practice
  • JOINs are risky and should be avoided where possible

What does it mean for a summary table to be "rebuildable", and what does that buy?

  • It is included in the nightly backups, so it can be restored
  • One query over the source tables rebuilds it entirely, so drift is repairable
  • It updates itself automatically whenever a booking is inserted
  • Its own constraints prevent it from ever disagreeing with the bookings table at all

After daily_sales is added, where does the truth about Tuesday's revenue live?

  • In daily_sales, since it was built specifically to hold revenue
  • In both tables equally — that's why the copy exists
  • In bookings and screenings — the summary is a derived copy
  • In whichever table was updated most recently

Denormalization "invites the update anomaly back in on purpose". What makes that acceptable here when it was a disease in Topic 26?

  • Summary tables never actually drift in practice
  • The duplicated data is small, so the anomaly barely matters
  • The risk is named and contained: one source is the truth and the copy rebuilds
  • Constraints placed on the summary table would catch any disagreement automatically

You got correct