intermediate
Joins
Select inner, outer, semi, and anti join shapes by cardinality, null behavior, and query plan cost.
Joins combine rows from related tables on a predicate — usually equality on keys. The join type controls which rows survive when matches or nulls appear on either side.
| Join | Keeps rows when | |------|-----------------| | `INNER JOIN` | Match exists on both sides | | `LEFT JOIN` | All left rows; null-extended right if no match | | `RIGHT JOIN` | Mirror of left — rare in practice | | `FULL OUTER` | All from both; nulls where no match | | Semi (`EXISTS`, `IN`) | Left rows with at least one match — no duplicate expansion | | Anti (`NOT EXISTS`) | Left rows with no match |
-- Users with at least one order (semi-join shape)
SELECT u.id, u.email
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- Users who never ordered (anti-join)
SELECT u.id FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
Cardinality matters: joining one-to-many without aggregation duplicates parent facts — wrap in a subquery or use `DISTINCT ON` / window functions deliberately. Filter early; push predicates that reduce join width before the join when the planner allows.
On interviews: given a report requirement, pick inner vs left vs exists and explain duplicate risk and null handling.
Common pitfalls: implicit comma joins without clear predicates; `LEFT JOIN` then `WHERE right.col = X` (turns into inner); `OR` join conditions that block index use; joining wide tables before filtering.
The trade-off is balancing readable SQL against plan cost — semi/anti joins often beat `DISTINCT` after an inner join, but the readable shape depends on engine and statistics.
Checklist:
- State cardinality (1:1, 1:N, N:M) for the join.
- Pick join type from null and duplicate requirements.
- Prefer `EXISTS` over `IN` for correlated semi-joins at scale.
- Filter driving table rows before the join when possible.
- Mention index needs on join keys.