Subqueries: a Query Inside a Query
Some of Lora's questions are naturally two questions stacked on top of each other. "Which films run longer than the average film?" cannot be answered in one thought — first you need the average, then you can compare each film against it. SQL has a shape for exactly this: a subquery, a complete query in parentheses whose answer feeds the query around it.
The idea rests on a promise planted quietly back in Topic 10: every query returns a table. If a query's result is a table, then anywhere a query expects a value or a set of values, another query can stand in and supply them. Queries can eat queries — and once you see it, half of everyday analytical SQL falls open.
The Scalar Subquery
Picture answering the films question by hand. You'd work out the average runtime first, jot it on a scrap of paper — 102 minutes — and then go down the list of films comparing each one against the scrap. The scrap is the subquery: a small side-calculation whose one result the main task consumes. That's the whole mental model, so let's write it in real terms.
SELECT title, runtime_min FROM films WHERE runtime_min > (SELECT AVG(runtime_min) FROM films);
The inner query, SELECT AVG(runtime_min) FROM films, collapses the whole films table to a single number: 102. The outer query then behaves exactly as if you had typed WHERE runtime_min > 102 — the subquery's answer is used like a literal value. On the Marquee's three films, only one clears the bar:
| title | runtime_min |
|---|---|
| The Long Rain | 112 |
A subquery that boils down to one row, one column — one value — is called a scalar subquery, and it can stand anywhere a single value could: after a comparison in WHERE, even in the SELECT list. One honest edge to know now rather than later: if a query in that position returns more than one row, the database refuses, because a comparison needs one value to compare against.
IN and Lists
A subquery doesn't have to shrink to one value — it can hand back a whole column of them, and the outer query can test membership against that list with IN. Lora's question: which films have screenings on screen 2? The inner query gathers the film ids that appear in screen 2's schedule; the outer keeps the films whose id is in that set.
SELECT title FROM films WHERE film_id IN (SELECT film_id FROM screenings WHERE screen = 2);
| title |
|---|
| The Long Rain |
| Paper Lanterns |
Read IN as the plain question "is this value in that set?" — for each film, true keeps the row, false drops it, the same row-by-row thinking as every WHERE since Chapter 3. The inner query built the set; the outer query interrogated it.
Subquery or JOIN?
Sharp readers will have noticed that the screen-2 question smells like a JOIN — films connected to screenings through film_id is an arrow straight off the schema diagram. Correct, and here is the same question written both ways:
-- as a subquery: screenings only filter the films SELECT title FROM films WHERE film_id IN (SELECT film_id FROM screenings WHERE screen = 2); -- as a JOIN: DISTINCT collapses repeat matches SELECT DISTINCT f.title FROM films f JOIN screenings s ON s.film_id = f.film_id WHERE s.screen = 2;
Both return the same titles, and the choice between them is taste more than law — but the taste has a rule of thumb worth carrying. Reach for a JOIN when you need columns from both sides in the answer (the title and the showtime). Reach for a subquery when one table merely filters the other and contributes nothing to the output, as screen 2's schedule does here. Fluent readers use both shapes daily and translate between them without ceremony.
Don't Nest for Sport
Since queries can eat queries, nothing stops a subquery from containing its own subquery, and so on downward. Resist. Two levels read fine; five read like a legal document, and knowing where to stop is part of the craft. When a question genuinely needs many stacked steps, grown-up SQL has a kinder tool — WITH … AS, which lets you name each step before using it. Its name is all this book will drop; the deep-dive course picks it up properly.
One performance worry, defused before it forms: "the inner query runs once per row, so this must be slow." Usually not — the engine typically computes an independent inner query once, or quietly rewrites the whole thing into a join-like plan. Write the version that says what you mean most clearly, and measure before optimizing; that is Chapter 8's spirit, one chapter early.
- "The subquery runs once per row, so it's always slow." The engine usually computes an independent subquery once, or transforms the query into an efficient plan. Write the clear version first; measure later.
- "Subqueries and JOINs are rivals — one of them is the 'correct' way." They are different emphases of the same relational move. JOIN when you need columns from both sides; subquery when one side only filters. Fluent readers use both.
- "Queries in parentheses are advanced SQL." It's the same SELECT you've written since Chapter 3, placed where a value goes. Nothing new was learned to write this page's queries — only rearranged.
- "A scalar subquery can return several rows and SQL will pick one." It won't — a comparison needs exactly one value, and an inner query that returns more than one row makes the database refuse the whole query.
- "Above average", "in the top group", "not in this set" — thresholds and membership tests are the everyday analytical questions of every data-adjacent job, and subqueries are their native shape.
- "Every query returns a table" graduates here from trivia to superpower: it's the reason queries compose, and composition is what separates writing SQL from merely reciting it.
Knowledge Check
In WHERE runtime_min > (SELECT AVG(runtime_min) FROM films), what does the inner query hand to the outer one?
- A copy of the whole films table
- One number, used as if you had typed it after the >
- A list of every film's runtime
- A corrected version of the outer query's entire WHERE clause
When is IN (SELECT …) the natural shape for a question?
- When you need to test whether a value belongs to a set another query builds
- When the answer must show columns drawn from both of the tables side by side
- When you need to compare each row against a computed average
- Only when the values being tested are numbers
You need each customer's name next to the title of every film they've booked. Subquery or JOIN — and why?
- Subquery — an IN can pull the matching titles into the customer rows
- JOIN — the answer needs columns from both sides in the same row
- Either — the two shapes always produce identical answers
- Neither — SQL needs two separate queries for this
A colleague warns that your subquery "re-runs for every row of the outer table, so it will be slow." What's the honest response?
- "True — I'll rewrite every subquery as a JOIN to be safe"
- "Performance never matters — the database handles everything"
- "The engine usually computes it once — write it clearly, then measure"
- "Subqueries always bypass the query planner, so they run slower every time"
You got correct