SQL theory · page 3 of 10

Joins and relationships

One to many, many to many, the kinds of join and the fan-out trap.

Data about one thing is spread over several tables, so most questions need a join: rows of two tables combined where a condition holds, usually a foreign key matching a primary key.

Relationships

  • One to many (1:N): a customer has many policies, each policy has one customer. The foreign key sits on the 'many' side: policies.customer_id.
  • Many to many (N:M): a policy has many coverages, and a coverage type appears on many policies. It needs a table in between, policy_coverages, with a foreign key to each side.
  • One to one: rare; usually one table is enough.

Kinds of join

JoinKeepsUse it for
INNER JOINonly rows with a match on both sidesclaims with their policy
LEFT JOINevery row of the left table; nulls where the right has no matchevery broker, with their policies if any
FULL JOINevery row of both sidescomparing two lists
CROSS JOINevery combination of rowsbuilding a grid, such as years × development years

Watch a join, row by row

SELECT b.name AS broker, p.policy_number
FROM brokers b
LEFT JOIN policies p ON p.broker_id = b.broker_id
ORDER BY b.broker_id, p.policy_id;
In the playground, WITH puts these few rows in place of the real tables, so PostgreSQL answers on exactly the rows shown here. Try it in the playground
Playback speed
  • The row being read
  • A row with the same key
  • Dropped

The final result as a table
What PostgreSQL returns: 4 rows
brokerpolicy_number
Oranje AdviesHM-011
Oranje AdviesHM-012
Tatra BrokersNULL
Alpen MaklerHM-013

USING (policy_id) is a short form of ON a.policy_id = b.policy_id when the column has the same name in both tables.

Semi-joins and anti-joins

EXISTS (subquery) keeps rows that have at least one match, without repeating them (a semi-join); NOT EXISTS keeps rows with none (an anti-join).

SELECT COUNT(*) AS customers_who_claimed
FROM customers cu
WHERE EXISTS (
    SELECT 1
    FROM policies p
    JOIN claims c USING (policy_id)
    WHERE p.customer_id = cu.customer_id
);
Each customer is counted once, however many claims they made. Try it in the playground

The fan-out trap

Joining a row to many rows repeats it. Sum a policy's premium after joining it to its claims, and a policy with three claims counts its premium three times. Aggregate each table to the level you need first (in a subquery or CTE), then join the totals.

SELECT COUNT(*) AS rows_after_join,
       COUNT(DISTINCT p.policy_id) AS policies
FROM policies p
JOIN claims c USING (policy_id);
More rows than policies: some policies have several claims. Try it in the playground
Before a join, ask: for each row on the left, how many rows can match on the right? If more than one, totals of left-side columns will be inflated.
Practise it on Day 3

Joins at a glance

A is the left table, B the right: at Harbour Mutual, policies and claims. The amber area is what the join returns.

  • a INNER JOIN b

    Only the rows that match in both tables: policies that have claims.

  • a LEFT JOIN b

    Every row of A, with B's columns where there is a match and nulls where there is none: every policy, with its claims if any.

  • a RIGHT JOIN b

    Every row of B, matched to A where possible. The same as a left join with the tables swapped.

  • a FULL JOIN b

    Every row of both tables, matched where possible and nulls on the side that has no match.

  • a LEFT JOIN b … WHERE b.key IS NULL

    The rows of A with no match in B: policies without a claim. NOT EXISTS does the same.

Check your understanding

Answer every question, then check.

1. Where does the foreign key go in a one-to-many relationship?

2. You need every broker, including those with no policies. Which join?

3. After joining policies to claims, SUM(p.annual_premium) is too high. Why?

An unhandled error has occurred. Reload 🗙

Reconnecting…

Your connection to Data Analytics SQL Lab dropped, probably a Wi-Fi blip. Hold on while it reconnects. Your work is kept.

The connection could not be restored. The page will reload in a few seconds.