Skip to content
Back to Shared account ownership
Browse Data ModelingNormalization

Normalization

Store each fact once, so an update cannot make the copies disagree.

8 min read

Overview

This is orders, a small table that records purchases. Each row holds the order itself along with the name and email of the customer who placed it, and because Dana has placed two orders, her email appears on two separate rows.

orders
order_idcustomeremailitemamount
101Danadana@brightmail.comDesk lamp42
102Raviravi@postbox.netMonitor arm89
103Danadana@brightmail.comCable kit18
104Meimei@fastmail.orgDesk lamp42

In this guide the word fact means one recorded piece of information, such as the statement that Dana’s email is dana@brightmail.com. That is a narrower meaning than the fact tables of dimensional modeling, which arrive later in this track and describe something else entirely.

The design above works for reading and for inserting, and nothing about it looks wrong on the surface. Normalization exists because of one question.

What happens when a fact stored in two places has to change?

The failure

The animation below shows the failure this design invites. Dana changes her email address, so the application issues an update against her first order, row 101. Diagnose the repeated fact before revealing the normalized structure. Identify the value that appears in two order rows, name the customer entity that determines it rather than the customer display name, and decide where that fact should live. As you step through the update, watch row 103, which still holds a copy of the old address.

Watch one update split the truthDana appears on two rows, and her email is stored on both of them.
the table as it stands
orders
order_idcustomeremailitem
101Danadana@brightmail.comDesk lamp
102Raviravi@postbox.netMonitor arm
103Danadana@brightmail.comCable kit
104Meimei@fastmail.orgDesk lamp
email is a fact about the customer

The update did exactly what it was asked to do. It changed the one row that matched its condition and reported success, while the second copy of Dana’s email sat outside the condition and kept the old value. The table now holds two different answers to the same question, and this failure is called an update anomaly.

The same design produces two related failures. A new customer cannot be recorded until they place an order, because customer information only lives inside order rows, and deleting a customer’s last order erases everything the table knew about them. Database texts call these the insertion anomaly and the deletion anomaly, and all three grow from the same root.

You can confirm the update anomaly directly by asking the corrupted table for Dana’s email.

select distinct email
from orders
where customer = 'Dana';

The query returns two rows where there should be one. No error was raised at any point, the update reported the expected row count, and the inconsistency will now sit in the table until someone happens to notice it.

The rule

Look again at what each column of orders describes. The item and amount columns state facts about the order itself, which is exactly what the row’s key identifies. The email column is different, because an email address is a fact about the customer rather than about any single order. Storing it on every order row is what created the copies, and the copies are what allowed one update to split the truth.

This relationship between columns is called functional dependency, and it says that a column’s value is determined by something. Dana’s email is determined by the customer’s stable identity, not by the customer display name. The amount is determined by the order. A table is soundly designed when every non-key column is determined by that table’s key. William Kent, an early researcher of the relational model, condensed this into a summary that database courses still teach. A non-key column should state a fact about the key, the whole key, and nothing but the key. Your diagnosis is complete when it names the dependency and separates facts without losing the relationship: the customer’s stable identity determines email, customers stores it once, and orders retains the customer_id reference.

The method

The normal forms turn Kent’s test into a sequence of checks, and each one targets a different way a column can depend on the wrong thing.

  1. 1
    First normal form: one value per cell

    A cell that holds a packed list, such as '555-0101, 555-0102', cannot be queried as individual values. First normal form requires one value per cell, which in practice means giving repeating values their own rows.

  2. 2
    Second normal form: no facts about part of the key

    Second normal form applies when a table has a composite key. In a line-items table keyed by (order_id, product_id), a product’s name is determined by product_id alone rather than by the full key, so it belongs in a products table instead.

  3. 3
    Third normal form: no facts about a non-key column

    Third normal form covers the failure you watched above. Dana’s email is determined by the customer’s stable identity, and that identity is not what the key of orders identifies. Customer facts move to a customers table and orders keeps a customer_id reference.

customers
customer_idnameemail
1Danadana@lumen.io
2Raviravi@postbox.net
3Meimei@fastmail.org
orders
order_idcustomer_iditemamount
1011Desk lamp42
1022Monitor arm89
1031Cable kit18
1043Desk lamp42

After the split, each fact has exactly one home. Higher forms exist beyond these three, and Boyce-Codd normal form in particular tightens an edge case involving overlapping composite keys, but third normal form is the working standard this guide holds you to.

The judgment

Normalization carries a cost of its own, because every table you split off becomes a join that future queries have to pay for. Weighing that cost honestly is the judgment this guide exists to teach. An anomaly is a correctness bug that silently corrupts recorded history, while an extra join is a performance cost that can usually be bought back with an index or a cache. Speed can be recovered after the fact, but a history that was recorded wrong often cannot be.

The default therefore follows the write path. In a system that accepts writes, stay at third normal form unless you can name the specific dependency you are breaking and point to the measurement that justifies breaking it.

Analytical models are the deliberate exception to that default. A star schema flattens customer facts into one wide dimension table, because a warehouse is read-heavy and is rebuilt from source data, which moves the anomaly risk out of the table and into the pipeline that loads it. That is the same trade evaluated under different costs, and this track covers it in the fact vs dimension guide.

Pitfalls

The update that lies

The anomaly never raises an error. The update succeeds and reports the expected row count, and the table is simply left inconsistent with itself. The problem tends to surface weeks later, when two reports disagree about the same customer and someone has to work out why.

  • Over-splitting into name-value rows. A generic attributes table of name-value pairs technically passes every normal form. In exchange it defeats column types, constraints, and the query optimizer.
  • Normalizing a reporting model out of habit. Snowflaking a warehouse dimension reintroduces joins that the read path pays for on every query. The write-path default does not transfer to a reporting model.
  • Trusting a uniqueness you never declared. The split only protects you if customers.customer_id is a key the database enforces. Declare the constraint, or the duplicates you removed will eventually find their way back in.
  • Storing derived values. An order total stored beside its line items is a copy of a computation, and copies drift apart over time. Either derive the total when it is needed, or treat the stored copy as a cache and give it a rebuild story.

Practice

Recap

  • Start write paths at third normal form: every non-key column should depend on the key, the whole key, and nothing but the key.
  • Check first normal form for packed cells, second for partial dependencies, and third for dependencies on non-key columns.
  • If one business fact can be edited in two places, expect an update anomaly and ask what cannot be inserted or deleted independently.
  • Denormalize only after measuring a read bottleneck, then document the duplicated truth and its synchronization rule.
  • A deliberate warehouse read model fits when query shape and measurement justify the extra copies.