Data Modeling/Schema Design

Intermediate8 min read

Normalization vs Denormalization

The classic tradeoff between eliminating redundancy for data integrity and introducing redundancy for query speed — and why analytical systems deliberately favor the opposite of what transactional systems want.

Normalization is the process of structuring tables to minimize redundant data, typically by splitting information into multiple related tables connected by foreign keys. Denormalization is the deliberate reverse: duplicating data across tables to avoid joins and speed up reads. Neither is universally 'correct' — they optimize for different things.

Why normalize

Imagine a single orders table that repeats the customer's name and email on every row. If a customer updates their email, you must update every order row, and any row you miss creates an inconsistency. Normalization (formally described through normal forms — 1NF, 2NF, 3NF) fixes this by storing each fact exactly once: a customers table holds the email, and orders merely references customer_id.

  • Eliminates update anomalies: a fact changes in exactly one place
  • Reduces storage of duplicated data
  • Enforces referential integrity through foreign keys
  • Costs you joins: reconstructing a full picture requires combining multiple tables

Why denormalize

Highly normalized schemas are great for systems that write data constantly and need strong consistency (an e-commerce checkout system). But analytical queries over normalized data often require joining eight or ten tables just to answer 'total revenue by region last month.' In analytics, data is usually written once (via a batch or streaming load) and read many times — so it often pays to denormalize: pre-join and flatten data into wide tables so a query engine can scan one table instead of joining many.

-- Normalized: three tables, a join needed to get a full picture
select o.order_id, c.email, o.amount
from orders o
join customers c on o.customer_id = c.customer_id;

-- Denormalized: the customer's email is duplicated onto every order row
-- so downstream queries need no join at all
select order_id, customer_email, amount from orders_wide;

Watch out — Denormalization is a deliberate tradeoff, not a shortcut

Denormalized tables can drift out of sync with their source of truth if the pipeline that builds them has bugs. Only denormalize in a layer you fully control and regenerate from source regularly — never denormalize the transactional system of record itself.

This is exactly why data warehouses are typically loaded via ELT/ETL pipelines from normalized operational databases: the operational system stays normalized for correctness, while the analytical copy gets deliberately denormalized (often into the dimensional models covered next) for query speed.