skip to content

What do denormalization and materialized views each do to reduce read load on a database, and what is the recurring cost both approaches share?

level: middleimportance: must knowfreq 55%

answer

  1. denormalize = duplicate data to skip joins
  2. matview = stored result of a query, refreshed periodically
  3. plain view = live query, no staleness but no speedup
  4. staleness is the shared cost
  5. CDC/triggers to keep duplicates in sync

basics

~20 s

Denormalization means storing some duplicate data together instead of spreading it across separate tables, so reads don't need expensive joins. A materialized view pre-computes and saves the result of a query so future reads just fetch the saved answer instead of recalculating it. Both trade extra storage and a risk of outdated data for faster reads.

solid answer

~50 s

Denormalization duplicates data across tables (e.g., storing a product's name directly on an order-line row instead of only a product_id foreign key) so a read avoids a join at query time, at the cost of extra storage and the need to update every duplicate copy when the source changes. A materialized view goes further: it stores the actual computed result set of a query (often an aggregation or multi-table join) as a physical table, refreshed on a schedule or trigger, so reads hit a pre-computed answer instead of re-running expensive joins/aggregations live. Both trade write-side complexity and storage for read-side speed, and both share the same core risk: the stored/duplicated data can drift out of sync with the source of truth between updates or refreshes, so consumers must tolerate some staleness or the system must invest in keeping them current (triggers, CDC, scheduled refresh).

go deeper

for a junior

Should grasp that denormalization means storing some duplicate data to avoid joins, and that this can go out of date.

for a middle

Should be able to explain both denormalization and materialized views concretely with an example, and name staleness as the core trade-off for both.

for a senior

Should be able to decide, for a given read pattern, whether denormalization, a materialized view, or neither is appropriate, and propose a mechanism (triggers, CDC, scheduled refresh) to bound staleness.

for a principal

Should reason about denormalization/materialized views as one option among several read-scaling techniques (alongside replicas, caching, federation) and make the call on where the staleness budget should live system-wide.

## The shared problem Both denormalization and materialized views exist to solve the same underlying problem: some reads are expensive because the data they need is spread across multiple normalized tables or requires heavy computation (joins, aggregations, sorting) to assemble, and doing that work on every single read wastes CPU and I/O on work that's often producing the same or similar answer repeatedly. Both techniques trade write-time or storage cost for read-time speed, but they do it at different granularities and with different mechanics. ## The baseline they push back against **Normalization** - the baseline both techniques push back against - organizes data to avoid duplication: a proper third-normal-form schema stores a fact exactly once and references it by foreign key elsewhere, which keeps data consistent (update it in one place, every reference sees the update) but means assembling a full picture often requires joining several tables together. ## Denormalization **Denormalization** deliberately reintroduces duplication to avoid some of those joins on hot read paths. A common example: - an e-commerce `order_line` table might normally store only a `product_id`, requiring a join to the `products` table to display the product name on an order history page; - a denormalized version stores `product_name` directly on the `order_line` row at the time of purchase. This is not only faster to read (no join), it's also often semantically correct in a way pure normalization isn't - the order should show the product name as it was when purchased, not the current name if the product was later renamed. The cost shows up on the **write side**: any process that creates or corrects that data now has to know about (and update) every duplicated copy, and it's easy for copies to drift if an update path is missed, which is a real production bug class - a customer's display name changes, and half the system reflects the new name while other tables still show the old one, because a denormalized copy was never touched by the update code path. ## Materialized views **Materialized views** operate at a coarser, more automated level: rather than a developer manually deciding to duplicate a specific column, a materialized view is defined by a query - often a join across several tables plus an aggregation, like 'total sales per product per day' - and the database (or a scheduled job) executes that query once and physically stores the result set as if it were a table. Subsequent reads of that data hit the stored, pre-computed rows directly instead of re-running the expensive join-and-aggregate query every time. This is especially valuable for reporting/analytics/dashboard queries that touch large amounts of data and would be far too slow to compute live on every page load, but are read far more often than the underlying data changes. PostgreSQL supports materialized views natively (`CREATE MATERIALIZED VIEW ... AS SELECT ...`, refreshed via `REFRESH MATERIALIZED VIEW`); many analytics/data-warehouse systems build on the same idea more elaborately. ## Refresh, and the freshness-versus-speed decision The refresh mechanism is where the shared risk concentrates: a materialized view's stored result is only as fresh as its last refresh, and refreshes cost real work - re-running the underlying query - so they're typically scheduled (every few minutes, hourly, nightly) rather than continuous. Between refreshes, the view can show numbers that don't match the live source tables, which is fine for a sales dashboard that's acceptable at 15-minutes-stale, and very much not fine for, say, a real-time inventory count used to decide whether to accept an order. Standard (non-materialized) views don't have this problem - they're just a saved query definition, re-executed live every time, always consistent but with none of the performance benefit - so choosing between a plain view and a materialized view is explicitly a freshness-versus-speed decision. ## The shared failure mode The failure mode both techniques share in production is **staleness surfacing where it wasn't expected**: a denormalized copy that a forgotten update path left behind, or a materialized view refreshed on a schedule that a downstream consumer assumed was live. The mitigation pattern is also shared: be explicit about which reads can tolerate staleness (and how much) versus which must go to the source of truth, and invest proportionally in keeping duplicated/materialized data fresh - - via database **triggers**, - **change-data-capture (CDC)** pipelines that propagate updates to denormalized copies, - or simply documenting and monitoring refresh cadence so nobody is surprised. A concrete combined usage: an analytics dashboard uses a materialized view refreshed every 10 minutes for 'orders per hour' charts (acceptable staleness, huge read-cost savings versus live aggregation), while the same system denormalizes a customer's shipping address onto each order row at checkout time (so historical orders show the address used at purchase, not whatever the customer's address happens to be today - correctness, not just performance, motivates that duplication).

  • How is a materialized view different from a regular (non-materialized) database view?
    A regular view is just a stored query definition that gets re-executed live every time it's read, so it's always consistent with current data but offers no performance benefit over running the query directly. A materialized view actually stores the query's result set as physical data, giving fast reads but requiring an explicit refresh to stay current.
  • What's a practical way to keep a denormalized copy of a field in sync with its source without manually updating every location?
    Use a database trigger on the source table that propagates changes to the denormalized copies, or a change-data-capture (CDC) pipeline that streams updates from the source table to every downstream consumer that holds a duplicate. Both automate the sync step so it doesn't depend on every code path remembering to update every copy.
  • When would denormalizing data be the wrong choice even if it would speed up reads?
    When the data changes frequently and must always be exactly current for correctness - e.g., an account balance used to authorize a transaction - denormalizing a stale copy risks acting on wrong data, so the read cost is worth paying to guarantee freshness. It's also a poor fit when write volume and the number of duplicate locations are both high, since the sync burden can outweigh the read savings.

Denormalization is like writing a phone number on a sticky note next to your desk instead of looking it up in the address book every time; a materialized view is like printing out a whole pre-calculated report once an hour instead of recalculating it from scratch on every request - both save time later but can go out of date.

saying these in an interview costs you the question

  • Thinks denormalization and normalization are the same trade-off in reverse with no cost
  • Confuses a materialized view with a regular view
  • Doesn't mention staleness as a risk of either technique
  • Assumes materialized views refresh automatically and instantly on every underlying change
  • Can't name a mechanism for keeping denormalized copies in sync

context