Cloud Tech by Victor
DatabasesIntermediate

Database Normalization

What 1NF, 2NF, and 3NF actually require, why normalization removes update anomalies, and when denormalizing on purpose is the right call.

Updated 2026-07-103 min read

Overview

Normalization is the process of structuring a relational schema so each fact is stored exactly once. It works through a series of normal forms (1NF, 2NF, 3NF, and beyond), each one closing off a specific kind of redundancy. The payoff is data integrity: updates touch one row instead of many, and it is structurally impossible for two copies of the same fact to disagree. The cost is more tables and more joins to reassemble a full picture, which is why real systems normalize for correctness first, then denormalize specific hot paths on purpose.

Quick Reference

FormRequiresFixes
1NFAtomic columns, no repeating groupsValues-in-a-column-name and comma-packed lists
2NF1NF, plus no partial key dependencyNon-key columns depending on only part of a composite key
3NF2NF, plus no transitive dependencyNon-key columns depending on other non-key columns
BCNF3NF, plus every determinant is a candidate keyRare edge cases 3NF still allows

Syntax

sql
-- Unnormalized: repeating groups packed into columns
CREATE TABLE orders_bad (
  id INT PRIMARY KEY,
  customer_name TEXT,
  customer_email TEXT,
  item_1 TEXT, item_1_qty INT,
  item_2 TEXT, item_2_qty INT
);

-- Normalized: one fact per table, related by foreign keys
CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL
);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  customer_id INT REFERENCES customers(id)
);

CREATE TABLE order_items (
  id SERIAL PRIMARY KEY,
  order_id INT REFERENCES orders(id),
  product_id INT REFERENCES products(id),
  quantity INT NOT NULL
);

Examples

sql
-- Reassembling a full order view requires joining back across the split tables
SELECT o.id, c.name, c.email, p.name AS product, oi.quantity
FROM   orders o
JOIN   customers c ON c.id = o.customer_id
JOIN   order_items oi ON oi.order_id = o.id
JOIN   products p ON p.id = oi.product_id
WHERE  o.id = 42;
sql
-- A deliberate, tracked denormalization: cache the order total to avoid
-- summing line items on every read of a hot "recent orders" list.
ALTER TABLE orders ADD COLUMN total_amount NUMERIC(10, 2);

-- Kept in sync via a trigger or application-layer write, not left to drift
UPDATE orders o
SET total_amount = (
  SELECT SUM(oi.quantity * p.price)
  FROM order_items oi JOIN products p ON p.id = oi.product_id
  WHERE oi.order_id = o.id
)
WHERE o.id = 42;

Visual Diagram

Common Mistakes

  • Treating 3NF as a universal target rather than a starting point, over-normalizing a read-heavy analytics table can force expensive joins on every query.
  • Storing a derived value (like city alongside zip_code) without recognizing it as a transitive dependency waiting to go stale.
  • Denormalizing without a consistency plan, a duplicated value with no trigger or application logic to keep it in sync will eventually drift.
  • Comma-packing multiple values into a single column (tags: "a,b,c") instead of a proper join table; this breaks 1NF and makes filtering/indexing painful.

Performance

  • Every additional normalized table is an additional join at read time, for OLTP workloads with selective indexed joins this is usually cheap, but it adds up on wide analytical queries.
  • Denormalized read models (materialized views, cached aggregates, or a dedicated reporting schema) are a common, legitimate way to keep a normalized source of truth while still serving fast reads.
  • Foreign keys enforce referential integrity but also add index maintenance cost on the referencing table, worth it almost always, but not free.

Best Practices

  • Normalize to 3NF by default for transactional (write-heavy, correctness-sensitive) schemas.
  • Denormalize deliberately and narrowly, one cached column or one materialized view, not a wholesale flattening of the schema.
  • Document any intentional redundancy and how it is kept consistent, so the next engineer does not "fix" it into a bug.
  • Reach for a separate read-optimized model (a reporting table, a search index, a cache) before denormalizing your system of record.

Interview questions

What problem does normalization solve?

Normalization removes redundant data by splitting it across related tables, which prevents update anomalies: without it, the same fact (say, a customer address) can be duplicated across many rows, so an update has to touch every copy or the data silently goes inconsistent. Normalization also prevents insertion anomalies (needing unrelated data to exist before you can insert a new fact) and deletion anomalies (losing unrelated data as a side effect of deleting one row).

What is the practical difference between 2NF and 3NF?

2NF removes partial dependencies: every non-key column must depend on the whole primary key, not just part of a composite key. 3NF goes further and removes transitive dependencies: a non-key column cannot depend on another non-key column. A classic 3NF violation is storing both zip_code and city on an orders table, where city is really determined by zip_code, not by the order itself, city belongs in its own lookup table.

When is denormalizing a good idea?

When read performance matters more than write simplicity and the redundancy is deliberately managed, for example, caching a computed total on an orders row instead of summing line items on every read, or duplicating a display name to avoid a join on a hot path. The key is that it is a conscious trade-off with a plan for keeping the duplicate data consistent (triggers, application logic, or accepting eventual consistency), not an accident.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement