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
| Join | Keeps | Use it for |
|---|---|---|
INNER JOIN | only rows with a match on both sides | claims with their policy |
LEFT JOIN | every row of the left table; nulls where the right has no match | every broker, with their policies if any |
FULL JOIN | every row of both sides | comparing two lists |
CROSS JOIN | every combination of rows | building 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;
- The row being read
- A row with the same key
- Dropped
The final result as a table
| broker | policy_number |
|---|---|
| Oranje Advies | HM-011 |
| Oranje Advies | HM-012 |
| Tatra Brokers | NULL |
| Alpen Makler | HM-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
);
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);
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 bOnly the rows that match in both tables: policies that have claims.
a LEFT JOIN bEvery 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 bEvery row of B, matched to A where possible. The same as a left join with the tables swapped.
a FULL JOIN bEvery row of both tables, matched where possible and nulls on the side that has no match.
a LEFT JOIN b … WHERE b.key IS NULLThe rows of A with no match in B: policies without a claim.
NOT EXISTSdoes the same.
Check your understanding
Answer every question, then check.