Skip to content
Learn/Relational foundations

Relational foundations

Learn each concept by watching it work, then put it into practice.

On this page

Bridge tables

When a many-to-many relationship needs its own grain, and when a direct foreign key is enough.

A bridge table turns a many-to-many relationship into an entity with its own grain. Use one when the relationship has history, attributes, allocation weights, or repeated rows that a direct foreign key cannot preserve.

Direct relationships

A direct foreign key is enough when one row points to one stable parent and the relationship does not need its own attributes.

Good fit:

  • one account has one current primary owner
  • one order line belongs to one product
  • one ticket has one current queue when history is out of scope

Common mistake: adding a bridge for every join. Extra bridge tables create unnecessary grain and make simple questions harder to query.

Bridge tables

A bridge table fits when both sides can repeat and the relationship itself is what analysts need to study.

Good fit:

  • accounts with multiple owners and split credit
  • orders with multiple promotions
  • support tickets handled by several agents over time
  • patients with several diagnoses on one encounter

Common mistake: drawing a direct N:M line without a table to hold the relationship grain. If the relationship has assigned_at, allocation_weight, effective_from, or reason, it needs a row of its own.

Allocation and history

Many bridge rows are factless facts. They may contain only keys and dates, or they may carry measures such as allocation percent or allocated discount.

When a measure can be counted through multiple bridge rows, protect the query from double counting. Either allocate the measure across bridge rows or make the metric definition explicit about distinct counting.

Kimball multivalued dimension bridge guidance is the pattern source for this choice. dbt semantic modeling reinforces the same habit from the metrics side: metrics need entities, dimensions, measures, and relationship grain that make joins unambiguous.

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. Before you press play, decide what you expect the table to say afterward when it is asked for Dana’s email. As you step through it, watch row 103, which still holds a copy of the old address. Then check the table’s answer against your prediction.

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, while 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.

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, and the customer is not what the key of orders identifies, so 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.