Page 1 of 15 palette size
SQL · Garden
a path between two beds
❋
— Chapter —
IV

The Flight Path

in which a bee flies the path from hive to bed,
pairing each visitor with the flower it has visited
❋
Summer · Flight Paths · Chapter IV of IX

Page 2 · Welcome

A garden has many beds

and most things you want to know live in more than one of them

In Chapter I you walked a single bed — the rose bed — and learned to fill a basket with flowers that matched your criteria. But the garden is more than its flowers. Bees visit them. Each bee remembers the flowers it has touched.

Which bee visited which flower? Which flowers haven't been pollinated yet? Which bees are new to the garden and haven't found a flower at all? Every one of these crosses between two tables. You need both sides to answer it.

That's what this chapter is about: the JOIN — the path between two tables, and a bee flying it, pairing each visitor with the flower it has visited.

The wider garden: multiple beds, paths between them.

Page 3 · The bees

The bees

three bees. each has an id.

The first new table is called bees. It holds the pollinators that visit the flowers — three of them, each with her own habits:

  • Buzz — the focused one. She picks a single flower and stays with it.
  • Honey — the productive one. She travels widely and visits more than one bloom.
  • Bumble — the wanderer. She's still looking for her first landing of the day.

Before we look at the table, here they are in the air:

Buzz the focused one Honey the productive one ? Bumble the wanderer
Three bees, left to right. Each is one row in the bees table below.

Just like a flower's tag, each bee has an id (a unique number) and a name. Nothing more, for now.

bees
idname
1Buzz
2Honey
3Bumble
The bees table. Three rows, two columns.

By themselves the bees aren't very interesting — you can't tell from this table alone which flowers they have visited. For that, you'll need a second table.

Page 4 · The flowers

The flowers

four flowers. each remembers who visited it.

The second table is called flowers. It holds the flowers in the bed — four of them: a Marigold, a Jasmine, a Lotus, and a Tulip. Before we look at the table, here they are in the bed:

Marigold Jasmine Lotus Tulip
Four flowers in the bed, left to right. Each is one row in the flowers table you're about to meet.

Each flower has a name and — this is the new part — a pollinated_by column that holds the id of the bee that has pollinated it. It's a pointer back into the bees table.

flowers
namepollinated_by
Marigold1
Jasmine2
Lotus2
Tulip∅ (null)
The flowers table. Notice that the Tulip's pollinated_by is empty — no bee has reached it yet.

Two things to notice. First: the Marigold has pollinated_by = 1, and bee number 1 in the other table is Buzz. So the Marigold was visited by Buzz — even though her name doesn't appear in this table at all.

Second: the Tulip has pollinated_by = NULL (the garden word for "nothing" — no bee has reached it). It waits.

Also worth noticing: Honey (id 2) has visited two flowers. One row in bees, two rows in flowers that point back to it. That's normal — one bee, many visits.

Page 5 · The path between

The connection

a number in one table that means a row in another

Look at the two tables side by side. Notice how the pollinated_by in the flowers table is just a number that matches the id in the bees table.

That match is the connection. It's the flight path between the two. A bee can fly it in either direction.

The column that carries that match is called a foreign key — it holds a value that identifies a row in a different table. Here, pollinated_by is a foreign key that identifies a bee by its id.

bees id name 1 Buzz 2 Honey 3 Bumble flowers name pollinated_by Marigold 1 Jasmine 2 Lotus 2 Tulip NULL
Three flowers point at bees. The Tulip's pointer is blank — it points at no one yet.

The same connection, shown as a moment in the garden:

? Bumble (no flower) Buzz Honey no visitor Marigold Jasmine Lotus Tulip
Buzz visits the Marigold (one bee, one flower). Honey visits both the Jasmine and the Lotus (one bee, two flowers — the two dashed paths). The Tulip has no visitor yet. Bumble wanders above, not yet landed on anything.

Each arrow says "the bee that pollinated this flower is the row over there with that id." When you write a JOIN query, what you're really telling SQL is which arrows to follow.

Page 6 · Why two tables

Why not just one?

a small thought before we walk

You might be wondering: why not just put the bee's name directly into the flowers table? Something like:

flowers (the bad version)
namebee_name
MarigoldBuzz
JasmineHoney
LotusHoney
Tulip∅
It looks simpler, but it isn't.

Honey appears twice. The day the gardener decides to rename her — "Queen Honey", let's say — you have to find every row that says "Honey" and edit it. Miss one, and the garden disagrees with itself.

With the two-table version, Honey lives in exactly one row in bees. Change it once and every flower that points at her updates by association.

This is the heart of why databases use multiple tables: each fact gets stored exactly once, in the place it belongs, and pointers connect them.

Page 7 · INNER JOIN

The path: INNER JOIN

fly the path; pair the flowers

An INNER JOIN is the basic flavor. The bee flies the path between the bees and the flowers. For each bee, it looks across at the flowers that bee pollinated — and puts the pair into the basket. If there's no match on either side, the row is dropped.

Press Run query. Watch the ribbons connect each flower to its bee, and the basket fill with three pairs.

flight-path.sql
the flight path
INNER JOIN
Press Run query to fly the path.
Field guide: INNER JOIN keeps only the rows that have a pair. Bumble (no flower visited) and the Tulip (no bee visiting) get left out — not in the basket.

Page 8 · The ON clause

The ON clause

how the bee decides what pairs with what

Every JOIN has an ON clause. It's the rule the bee uses to decide which flower on one side pairs with which bee on the other.

Here we say: ON flowers.pollinated_by = bees.id. In plain English: "pair a flower with the bee whose id equals that flower's pollinated_by."

Try changing the ON to something nonsensical — say, ON bees.id = 999 — and watch every ribbon disappear. The bee has no rule that matches, so nothing pairs.

flight-path.sql
the flight path
ON ...
Run the query, then try changing the ON clause.
Field guide: The ON condition can be anything — usually it's a_table.foreign_key = b_table.id (the foreign_key column — the one that identifies a row in the other table), but you can compare against constants too. The bee just follows whatever rule you write.

A warning: if the ON clause doesn't compare a column on each side, every row pairs with every row — a cartesian product. Ten bees and four flowers would come back as forty rows.

Page 9 · LEFT JOIN

Don't lose a bee

LEFT JOIN: keep every flower on the left

Bumble hasn't visited a flower. An INNER JOIN drops her from the basket because there's no pair to make. But what if you want Bumble in the basket anyway — perhaps to know which bees haven't pollinated anything yet?

A LEFT JOIN keeps every row from the left-side table (bees), even those with no match on the right. Left means the table in your FROM; right means the table after JOIN. The unmatched ones get a NULL on the right — the garden word for "nothing here."

flight-path.sql
the flight path
LEFT JOIN
Run the query. Bumble appears in the basket, but her flower is NULL.
Notice: Bumble still hasn't visited a flower — the database can't invent one. So her pair is NULL. The Tulip (no bee) is still left out, because the LEFT side is bees.

Page 10 · RIGHT JOIN

Don't lose a flower

RIGHT JOIN: keep every flower on the right

Now flip the situation. You want every flower in the basket, even those without a bee. The Tulip, waiting unpollinated, should be there too. Use a RIGHT JOIN:

flight-path.sql
the flight path
RIGHT JOIN
Run the query. The Tulip comes home with NULL for its bee.
Mirror image of LEFT: a RIGHT JOIN is just a LEFT JOIN seen from the other side. You could rewrite this as FROM flowers LEFT JOIN bees ON ... and get the same rows. Most people prefer LEFT JOIN because it's easier to read.

Page 11 · FULL OUTER JOIN

Lose no one

FULL OUTER JOIN: keep everyone, paired or not

What if you want everyone — every bee and every flower, paired up where possible and standing alone where not? That's a FULL OUTER JOIN.

The basket comes home with five rows: three real pairs, plus Bumble (no flower) and the Tulip (no bee), each with a NULL on the missing side.

flight-path.sql
the flight path
FULL OUTER JOIN
Run the query. Bumble and the Tulip both come home, each with their own NULL.
Field guide: FULL OUTER JOIN = LEFT JOIN + RIGHT JOIN, with the paired rows not duplicated. Nobody gets left in the bed.

Page 12 · Four flavors

The anatomy of a JOIN

four ways to walk the same path

You've now seen all four common flavors. The only difference between them is who gets left in the bed when there's no pair on the other side.

A note on portability: not every database speaks all four. MySQL has no FULL OUTER JOIN at all, and SQLite only gained RIGHT and FULL joins in version 3.39 (2022). Where they're missing, you can emulate FULL as a LEFT JOIN unioned with a RIGHT JOIN (or two LEFT JOINs with the tables flipped).

INNER JOIN
only paired rows come home. Both orphans dropped.
LEFT JOIN
every row from the left table comes home. Right-side orphans dropped. Left orphans get NULL on the right.
RIGHT JOIN
every row from the right table comes home. Left-side orphans dropped. Right orphans get NULL on the left.
FULL OUTER JOIN
everyone comes home. Orphans on either side get NULL where the pair would be.
INNER LEFT RIGHT FULL left circle = first table, right circle = second table, shaded = what comes home

Page 13 · Fly it yourself

Fly the path yourself

write any JOIN you like

The two tables are still there. Try any JOIN you can think of. The result table below shows the rows the basket comes home with.

To find rows where a joined column is blank, use IS NULL, not = NULL. NULL isn't a value that equals anything — including itself — so = NULL never matches. IS NULL is a predicate that asks about the value's presence, not its equality.

scratch.sql
the flight path
your query
Edit the query, then run.
⌘/Ctrl + Enter to run
Result —
Run a query to see rows here.

tables

bees(id, name)3 rows · Buzz, Honey, Bumble
flowers(id, name, pollinated_by)4 rows · Marigold, Jasmine, Lotus, Tulip
Try: SELECT bees.name FROM bees LEFT JOIN flowers ON flowers.pollinated_by = bees.id WHERE flowers.id IS NULL; — find the bee that hasn't visited any flower. (The query box above is already set to this!)

Page 14 · End of Chapter IV

The basket comes home

what you've learned, and what's next

In the garden of the Flight Path, two beds are walked at once. One holds the bees; the other holds the flowers. The gardener pairs them four different ways — by pairs only, by every bee, by every flower, and by everyone at once.

INNER JOIN
paired rows only — Bumble and the Tulip stayed out
LEFT JOIN
every bee — Bumble came along, her flower field blank
RIGHT JOIN
every flower — the Tulip came along, its bee field blank
FULL OUTER JOIN
nobody left behind
ON
the rule the bee uses to decide who pairs with whom
NULL
the garden word for "nothing here" — no flower, no bee

After this garden lies the second of Summer's two: it is called the Sorting Table. There a fresh basket of flowers is set down, and its contents are placed into smaller baskets — one color to each — and from each smaller basket a number is taken. For today, the pairs have been made; nobody is left behind; the path quiets, and the sun starts down.

Check your basket

Two questions to confirm JOIN feels natural. Write a query and press Check.

1. Find every bee paired with the flower it pollinated. Use INNER JOIN. Return bees.name and flowers.name.

SELECT bees.name, flowers.name FROM bees INNER JOIN flowers ON flowers.pollinated_by = bees.id;

2. Find every bee that has not pollinated any flower. Use LEFT JOIN + IS NULL. Return bees.name.

SELECT bees.name FROM bees LEFT JOIN flowers ON flowers.pollinated_by = bees.id WHERE flowers.id IS NULL;
· Sunset ·
❋
end of Chapter
IV
the bee returns to the hive,
each pair noted in the day's basket
❋
an intermission is recommended · Chapter V awaits