Normalization vs Denormalization
Normalization and denormalization are usually presented as opposites, but they answer the same underlying question from different directions: how much redundancy should this schema tolerate.
Search across all documentation pages
Normalization and denormalization are usually presented as opposites, but they answer the same underlying question from different directions: how much redundancy should this schema tolerate.
Normalization removes redundancy to protect correctness; denormalization reintroduces some of it, on purpose, to make specific reads faster.
This page explains the reasoning behind both, so the rest of this section's pages on normal forms, materialized views, and intentional denormalization read as tools on a spectrum rather than as competing camps.
JOINs at read time; denormalization costs consistency work at write time.Normalization is the process of organizing columns and tables so that each fact is stored in exactly one place.
It is driven by functional dependencies: if a value like product_name is fully determined by sku, then product_name belongs in the table keyed by sku, not copied onto every row that references that product.
The formal normal forms (1NF, 2NF, 3NF, and further) are named checkpoints along this process, each one closing off a specific category of redundancy.
The payoff for normalization is that an update anomaly becomes structurally impossible: a fact stored in one place cannot go out of sync with itself.
Denormalization deliberately violates that principle by storing a derived or duplicated value somewhere it did not strictly need to be, in exchange for avoiding a join at read time.
The two techniques are not stages of maturity, where normalization is "correct" and denormalization is a fallback; they are a genuine trade-off between write-time correctness guarantees and read-time cost.
-- Normalized: order_items has no duplicated product data
SELECT oi.order_id, p.name, p.sku
FROM order_items oi
JOIN products p ON p.product_id = oi.product_id;Most production schemas are not purely one or the other; they normalize the write-heavy core of the domain and denormalize a small number of specific, known-hot read paths.
The decision of where to sit on this spectrum should follow from a table's actual read/write ratio, not from a general preference.
A table written once and read thousands of times per write is a strong candidate for some denormalization, because the cost of keeping a duplicated value in sync is paid rarely relative to how often the shortcut pays off.
A table written and read at similar rates gains little from denormalization, because the synchronization cost and the join-avoidance benefit roughly cancel out.
PostgreSQL gives you a materialized view as a structured way to denormalize without abandoning the normalized tables as the source of truth.
CREATE MATERIALIZED VIEW order_totals AS
SELECT order_id, SUM(amount) AS total
FROM order_items
GROUP BY order_id;
-- Refreshed explicitly, not on every underlying write
REFRESH MATERIALIZED VIEW order_totals;The materialized view holds a denormalized, precomputed shape, but the underlying normalized tables remain the source of truth that the view is refreshed from.
That refresh step is the crux of every denormalization decision: someone has to decide how stale the denormalized copy is allowed to be, and what mechanism (a trigger, a scheduled job, an application-level write) keeps it within that bound.
A denormalized column with no defined refresh path is not a performance optimization, it is a slowly accumulating correctness bug.
At scale, the read/write ratio argument extends into the OLTP-versus-OLAP distinction that shapes an entire system's design, not just one table.
OLTP workloads (many small transactions, strong consistency needs) lean normalized, because correctness under concurrent writes matters more than shaving a join off a rare report.
OLAP workloads (large aggregate queries over historical data) lean denormalized, often into star-schema-style fact and dimension tables, because the data is written once by a batch process and read repeatedly by analysts.
Intentional denormalization in an OLTP system, done well, is narrow and documented: one specific column, one specific hot query, one specific stated consistency mechanism, rather than a general policy of duplicating data wherever it seems convenient.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Full normalization (3NF+) | No update anomalies; single source of truth per fact | Read paths may require several joins | OLTP tables with frequent, concurrent writes |
| Materialized view | Denormalized read speed with a normalized source of truth intact | Data can be stale between refreshes | Expensive aggregates read far more often than the source changes |
| Ad hoc denormalized column | Fastest possible read for that one column | Requires an explicit sync mechanism; easy to forget | A single, proven hot path with a clear owner |
| Star schema (fact/dimension) | Optimized for large aggregate scans | Poor fit for high-concurrency transactional writes | OLAP and reporting workloads |
The honest failure mode on this spectrum is not choosing denormalization, it is choosing it silently, without writing down the staleness tolerance or the refresh mechanism anywhere a future engineer can find it.
REFRESH, and PostgreSQL does not refresh it automatically unless something is scheduled or triggered to do so.Normalization stores each fact in exactly one place to prevent contradictions between duplicate copies.
Denormalization deliberately duplicates or precomputes data to avoid the join cost of reassembling it at read time.
Yes, that is the safer default order: normalize to establish a correct source of truth, then denormalize specific, proven hot paths once you have evidence they need it.
Denormalizing speculatively, before a real read pattern exists, adds risk without a matching benefit.
A functional dependency A -> B means the value of A fully determines the value of B.
Normalization uses functional dependencies to decide which table an attribute belongs in - if sku determines product_name, that name belongs with the SKU, not duplicated onto every row that references it.
A materialized view stores a denormalized, precomputed result while the underlying normalized tables remain the actual source of truth.
It gives you read-time speed without discarding write-time correctness, at the cost of the view being stale until refreshed.
As stale as the time between REFRESH MATERIALIZED VIEW calls, since PostgreSQL does not refresh one automatically on underlying writes.
The acceptable staleness window is a decision the team has to make explicitly, not a property PostgreSQL enforces for you.
In this section, yes - the reason to denormalize is almost always to avoid join cost on a specific, frequently read path.
Other reasons sometimes cited, like convenience, are usually a sign the denormalization was not deliberate enough.
Look at its actual read/write ratio: a table read far more often than it is written is a reasonable candidate for some denormalization.
A table read and written at similar rates usually gains little from it.
Doing it without a defined mechanism to keep the duplicated value in sync with its source.
An unsynchronized denormalized column silently drifts into wrong data over time.
No - OLTP systems generally lean normalized because concurrent write correctness matters most, while OLAP systems lean denormalized, often into star schemas, because the data is written in batches and read repeatedly for analysis.
3NF removes the anomaly categories that matter for most application schemas, and BCNF closes a narrower edge case involving overlapping candidate keys.
Most teams target 3NF by default and reach for BCNF only when that specific edge case is present.
Yes, and in practice they almost always do - most production schemas normalize their core write-heavy tables while denormalizing a small number of known, measured hot read paths.
Treating the choice as schema-wide rather than table-by-table is usually a mistake.
Confirm the read/write ratio actually justifies it, and write down the refresh or consistency mechanism before shipping the change.
A denormalization without a stated staleness plan is a bug waiting to surface, not a finished optimization.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17).
Reviewed by Chris St. John·Last updated Jul 15, 2026