Topic 19

Joining Three Tables and More

SQL

The Marquee's flagship question has been waiting since Chapter 1: who is coming to Night Bus on Friday, and in which seats? Look at where the pieces live and you'll see why it had to wait — the names are in customers, the seats in bookings, the showtime in screenings, the title in films. Four tables, one question.

The good news is that this page contains nothing new. JOINs chain: each JOIN adds one more table to the working answer, using the same "foreign key equals primary key" move you already own. A four-table query is three well-lit steps, not a leap — and the technique of taking those steps one at a time is what this page actually teaches.

One JOIN at a Time

Never write a big query in one breath. Build it, and say out loud what you have after each step. Step one is last page's territory — bookings with their customers' names:

Step 1 — bookings + customers: whose seat is whose
SELECT c.name, b.seat
FROM bookings b
JOIN customers c ON c.customer_id = b.customer_id;

After this step, each row is a booking wearing its customer's name: "Anna, seat G7". No showtime yet, no film. Step two follows the booking's other pointer, screening_id, into the screenings table:

Step 2 — add screenings: when, and on which screen
SELECT c.name, s.starts_at, s.screen, b.seat
FROM bookings b
JOIN customers c ON c.customer_id = b.customer_id
JOIN screenings s ON s.screening_id = b.screening_id;

Now each row reads "Anna, Friday 20:00, screen 1, seat G7" — a booking with its person and its showtime, still missing only the film's name. The screening row carries film_id, so step three follows that last pointer into films, and while we're there, filters down to the show Lora asked about:

Step 3 — add films, filter to Friday's Night Bus: the flagship query
SELECT c.name, f.title, s.starts_at, b.seat
FROM bookings b
JOIN customers c ON c.customer_id = b.customer_id
JOIN screenings s ON s.screening_id = b.screening_id
JOIN films f ON f.film_id = s.film_id
WHERE f.title = 'Night Bus'
  AND s.starts_at = '2026-05-15 20:00';
nametitlestarts_atseat
AnnaNight Bus2026-05-15 20:00G7
BenNight Bus2026-05-15 20:00G8
ChloeNight Bus2026-05-15 20:00G9

Four tables, and the query still reads top to bottom like a story: start from the bookings, name the person, name the show, name the film, keep Friday's Night Bus. Each JOIN line was one repetition of a move you learned two pages ago.

The Chain Follows the Schema

Here is the part that turns technique into confidence: you never have to invent a JOIN path, because the schema already drew it. The arrows from Chapter 2's diagram — bookings point at customers and screenings, screenings point at films — are exactly the roads a query may travel. Every ON clause in the flagship query is one of those arrows, written as an equality.

The JOIN path is the schema's own arrows — three steps along them
Flagship query: who is coming to Night Bus on Friday?
start herebookings
step 1 · customer_id→ customers
step 2 · screening_id→ screenings
step 3 · film_id→ films
Every ON clause is one schema arrow, written as an equality.

This works in both directions, and the reverse direction is your day-one skill at any job: handed someone else's five-table query, put the schema diagram beside it and trace each ON clause along an arrow. The query stops being a wall of syntax and becomes a route on a map you can follow with a finger.

Does JOIN Order Matter?

A natural worry: with three JOINs, did we have to write customers before screenings? For a chain of inner joins, no — the rows that come back are the same whichever order you list the tables in, as long as every ON clause still connects to something already introduced. And one honest sentence about what's under the hood: the database's query planner reorders joins as it sees fit anyway, choosing whatever sequence it computes to be fastest — Chapter 8 pays that sentence off properly.

So the order you write is for people, not for the machine. Start from the table your question is about, and add the others in the order that tells the story most plainly. The engine will do what it wants; your reader will thank you for the narrative.

Keeping It Legible

At four tables, style stops being taste and becomes survival, so name the three habits the flagship query quietly used. Alias every table, and use the aliases everywhere. Put each JOIN on its own line, with its ON immediately after it on the same line — one line, one arrow. And qualify every column in the SELECT list, so six months from now nobody has to guess which table starts_at came from. Shown, not preached: scroll back up and you'll find all three in every step.

One last reassurance, because beginners often read a long FROM clause as a symptom of something wrong. More JOINs do not mean worse design — the schema was built to be joined, and a five-table query over a clean schema beats a no-join query over one bloated table every time. The bloated table is where data rots; the joins are where it comes back together, on demand, correct.

Common Confusions
  • "Each extra JOIN multiplies the complexity." Each JOIN is the same FK-equals-PK move again. A four-table query is three repetitions of a thing you already know, not a new thing.
  • "Tables must be joined in the order they appear in the schema." Any path along the arrows works. The FROM order serves the human reader — start from the table the question is about.
  • "Lots of JOINs means the design is wrong." The opposite: the schema is designed to be joined. A clean schema plus a five-table query beats one rotten flat table with no joins, every time.
  • "The steps run one at a time, saving each intermediate table." The incremental steps are for you, the writer. The engine takes the finished query and builds one plan for the whole thing — no intermediate tables are stored anywhere.
Why It Matters
  • Real questions routinely span three to five tables. Building incrementally — one JOIN, then say what you have — is the professional technique for writing them without fear.
  • Reading a JOIN chain against a schema diagram is exactly how you'll decode other people's queries at work — the single most common SQL-reading situation in any data-adjacent job.

Knowledge Check

"Which films has Anna seen?" Which JOIN path answers it?

  • customers joined straight to films on customer_id = film_id, no hops
  • customers → bookings → screenings → films, one arrow at a time
  • customers → bookings → films, skipping screenings
  • Just one JOIN — SQL can't chain more than two tables

In a chain of INNER JOINs, what changes if you list the tables in a different order?

  • The result contains different rows
  • The query runs measurably slower in the wrong order
  • Only the readability — the rows are the same, and the engine reorders anyway
  • The database rejects any join order that doesn't match how the schema was built

After step 2 (bookings joined with customers and screenings), what does each row of the intermediate answer hold?

  • A booking with its customer's name and the film's title
  • A booking with its customer's name and its screening's time — but no film title yet
  • Only ids — names don't appear until the final step
  • One summary row for each screening, each carrying a running count of its bookings so far

A colleague sees your four-table query and says "so many JOINs — the database design must be bad." What's the accurate reply?

  • "True — I should merge these tables into one to avoid the joins"
  • "JOINs are a necessary evil until we can afford a better database"
  • "The schema is built to be joined — the query just walks its arrows"
  • "Four tables is unusual — most real queries never join more than two"

You got correct