Skip to main content
Normalization vs Denormalizationlesson 2 of 3 · 3 min read

Why Reads Push You to Denormalize

Three forces, all on the read path

Three forces bend teams away from a clean schema, and all three live on the read path.

Start with volume. Your page that joins posts to users to images runs fine at 50 requests a second. At 5,000, that join runs 5,000 times a second, and even a fast join burns processor and memory at that rate.

Do the same work once when you write instead of on every read, and you have the entire pitch for denormalising. A post is written once and read ten thousand times, so move the cost onto the rare side of that ratio.

Watch your tail latency next. A five-table join might average 20 milliseconds and spike to 800 when the planner picks a bad path for a user with unusual data, and your worst percentile lives on exactly those spikes. A single-row read from a pre-joined table has almost no variance left to spike.

Take sharding as the third and least negotiable. Once your posts and users live on different machines, your database cannot join them at all; your application fetches from both and stitches them together.

Every store that dropped joins made this force explicit, and the document databases turned it into doctrine you are expected to follow. Put the comments inside the post, store the author's name next to their identifier, and shape the record like the page that renders it.

The forms, smallest first

Escalate through the forms in order, taking the smallest one that works. A duplicated column. Then a running total kept up to date on write instead of counted on read. Then a whole query's result stored and refreshed. Then a fully pre-joined table built for one screen.

Denormalise on evidence, never on vibes. Explain the slow query, confirm the join really is the cost, and copy the narrowest set of fields that fixes it. Teams that skip the measuring usually find they duplicated data to speed up a query that was missing an index.

the shape of it
Feed requestpostsusersmediafeed_itemsprejoined at writejoin 1join 2join 3one indexed read
step 1 of 3
The denormalized table answers in one read what the normalized schema assembles with three joins per request.

Worked example

Priyanka owns the home feed at a recipe app doing 3,000 reads per second. The query joins posts, users, and media, and p99 has crept to 700 ms; EXPLAIN shows the join itself is the cost, indexes are all in place. She widens the posts table with author_name, author_avatar_url, and photo_thumb_url, populated at write time. The feed becomes a single index scan on one table: p99 drops to 60 ms and the database is 40 percent less busy. The trade shows its teeth three weeks later when a user changes their avatar and old posts keep the stale image until a backfill job catches them overnight. Product decides a day of stale avatars on old posts is fine. Priyanka writes that decision in the schema comment, because the next engineer will otherwise file it as a bug, which, without the comment, it is.