Dark atmospheric scene with SQL query elements and detective motifs

Struggling With SQL JOINs? Here's How to Make Them Finally Click

October 25, 2026•Daniel•5 min read

If JOINs are still giving you trouble months after you started learning SQL, you're in very good company. JOINs are "basic" only in the sense that they're taught early. They're the first time SQL asks you to think about two sets of rows at once, and that's genuinely a new way of thinking.

Most explanations use Venn diagrams, and Venn diagrams are a big part of why JOINs feel slippery. Here's a different approach that tends to make them click: forget the circles, and look at the actual rows.

Why Venn Diagrams Make JOINs Harder

The classic picture shows two overlapping circles, with INNER JOIN as the middle. It's fine as a reminder of which rows survive, but it hides the two things that actually confuse people:

  • Rows multiply. If one customer has three orders, joining them produces three rows, not one. Circles can't show that.
  • Unmatched rows get NULLs. A LEFT JOIN doesn't just "include" the left circle; it fills the missing right-side columns with NULL, which then affects your WHERE clause and your counts.

So instead of circles, here's the mental model that works: a JOIN looks at every possible pair of rows, and keeps the pairs where the ON condition is true. Then, depending on the JOIN type, it adds back rows that didn't pair with anything.

Watch It Happen With Real Rows

A small detective case. You have suspects and the badge scans from the night of a break-in:

-- suspects                 -- badge_scans
id | name                   scan_id | suspect_id | door
---+-------------           --------+------------+-------------
 1 | Mara Quinn                 101 |          1 | Front gate
 2 | Elias Grant                102 |          1 | Records room
 3 | Tobias Reed                103 |          3 | Front gate
                                104 |       NULL | Back door

INNER JOIN: keep the pairs that match

SELECT s.name, b.door
FROM suspects s
JOIN badge_scans b ON s.id = b.suspect_id;

Walk through it by hand. Mara (id 1) pairs with scans 101 and 102. Elias (id 2) pairs with nothing. Tobias (id 3) pairs with 103. Scan 104 pairs with nobody.

name        | door
------------+-------------
Mara Quinn  | Front gate
Mara Quinn  | Records room
Tobias Reed | Front gate

Mara shows up twice because she had two matching pairs. One output row per matching pair, not per suspect. Once you expect this, a lot of confusion disappears.

LEFT JOIN: same pairs, plus left rows that had no pair

SELECT s.name, b.door
FROM suspects s
LEFT JOIN badge_scans b ON s.id = b.suspect_id;
name        | door
------------+-------------
Mara Quinn  | Front gate
Mara Quinn  | Records room
Elias Grant | NULL
Tobias Reed | Front gate

The same three pairs, plus Elias, who had no match, with NULL in every badge_scans column. That's all a LEFT JOIN is.

RIGHT JOIN is the mirror image (it would add back scan 104 with a NULL name), and FULL OUTER JOIN adds back unmatched rows from both sides.

Four Habits That Make JOINs Click

1. Always ask "one row per what?" before joining

Before you write the JOIN, say what one row of each table represents: "one row per suspect" and "one row per badge scan." Then predict: after the JOIN, one row is per scan (with suspects repeated). That prediction tells you whether your later SUM or COUNT will be right.

2. Count rows before and after

Run SELECT COUNT(*) on each table, then on the joined result. If suspects has 3 rows and your LEFT JOIN returns 4, you know exactly why: one suspect matched twice. If it returns 30, something is matching far more than you thought, usually a missing condition in ON.

3. Look at the raw join before aggregating

Before writing GROUP BY, run the JOIN with SELECT * and LIMIT 20 and actually read the rows. Most JOIN bugs are obvious the moment you see the repeated or NULL rows with your own eyes.

4. Pick the JOIN by asking which rows must survive

  • Only rows with a match on both sides? INNER JOIN.
  • Every row from my main table, matched or not? LEFT JOIN, with the main table first.
  • Everything from both? FULL OUTER JOIN.

In practice you'll use INNER and LEFT for nearly everything. Many people never write a RIGHT JOIN, because any RIGHT JOIN can be rewritten as a LEFT JOIN by swapping the tables.

Fewer rows than expected? Something didn't match. More rows than expected? Something matched more than once.

The Two Patterns Worth Memorizing

Find rows with no match (the anti-join)

-- Suspects with no badge scan: their alibi can't be checked
SELECT s.name
FROM suspects s
LEFT JOIN badge_scans b ON s.id = b.suspect_id
WHERE b.scan_id IS NULL;

Returns Elias. This is the "customers who never ordered" query, and it's one of the most common interview questions there is.

Filter the right table without losing left rows

This looks like "all suspects, with their front-gate scans if any":

SELECT s.name, b.door
FROM suspects s
LEFT JOIN badge_scans b ON s.id = b.suspect_id
WHERE b.door = 'Front gate';

But Elias disappears. His b.door is NULL, and WHERE throws him out, turning your LEFT JOIN back into an INNER JOIN. Move the condition into ON to keep him:

SELECT s.name, b.door
FROM suspects s
LEFT JOIN badge_scans b
  ON s.id = b.suspect_id
 AND b.door = 'Front gate';

Rule: ON decides what counts as a match; WHERE filters the final result. For INNER JOINs it makes no difference. For LEFT JOINs it changes everything.

Taking a While Is Not Slow

People often call JOINs "a basic thing," as if struggling with them means something is wrong. It doesn't. JOINs combine three ideas at once (pairing rows, multiplication, NULLs), and almost everyone who uses SQL professionally remembers a period when they didn't trust their JOIN results. What fixes it isn't more explanation. It's writing a lot of JOINs on data small enough to check by hand, until predicting the output becomes a habit.

Practice JOINs With Something at Stake

In Murder in Blackwater Bay, you connect suspects, records and timelines using exactly these JOIN patterns, and the story only moves forward when your query finds the right rows. It's a lot easier to care about a missing row when it's a missing alibi.

Adventurer, your next quest awaits: