Page 1 of 15 palette size
SQL · Garden
a smaller basket gathered first, used inside the larger walk
❋
— Chapter —
VI

The Basket Inside the Basket

in which the gardener fills a small basket first
and carries it inside her larger one
❋
Autumn · Subqueries · Chapter VI of IX

Page 2 · Welcome

Questions one query can't answer

when the answer depends on the answer

Today the gardener stays in the flower_bed you met in Chapter V — the ten flowers (four Lotus, three Tulip, two Jasmine, one Marigold) are still on the sorting table, still where you left them. In this chapter she'll ask questions about individual flowers whose answers depend on facts about the whole bed.

Some questions need a smaller question answered first. Which flowers are taller than the average flower? To answer it you'd have to know the average — but the average isn't something you can stamp on a single flower's tag. It's a property of the whole bed.

You already know the verb that produces the average: SELECT AVG(height) FROM flower_bed. The trick is feeding that one number into a second query that uses it as a filter.

This is a subquery — a query nested inside another, in parentheses. The metaphor: the gardener fills a smaller basket first (the inner query's result), carries it with her, and consults it as she walks the bed for the larger basket.

avg=20.5
A smaller basket nested in the larger one. The inner one was filled before the walk began.

Page 3 · The smaller basket

A subquery, named by its parentheses

the inner basket runs first, the outer basket runs after

Look at this query. Anything inside the inner parentheses is a complete SQL query on its own — a normal SELECT. The database runs it first, gets a result, and then plugs that result into the outer query — unless the inner query mentions a column of the outer row, in which case it's recomputed once per outer row. We come to that on the correlated page.

SELECT species, height
FROM flower_bed
WHERE height > (SELECT AVG(height) FROM flower_bed);
                  ↑─── the smaller basket ──↑
Order of operations: inner first (computes the average, ~20.8), then outer (keeps flowers taller than that).

Three kinds of "shape" the inner basket might come back as, each used differently by the outer query:

  • One value (a single number or string) — used with comparison operators like >, =, !=
  • A list of values — used with IN (...) or NOT IN (...)
  • Any rows at all (yes/no) — used with EXISTS (...)

Each gets its own page next.

Page 4 · Scalar subquery

Taller than the average

a single number from the smaller basket

The simplest subquery returns one number. Here, the inner SELECT computes the average height across the bed — a single value, about 20.8. The outer SELECT then walks the bed and keeps only the flowers whose height is greater than that.

subquery.sql
the bed (with inner basket value)
scalar subquery
Press Run to compute the inner basket, then filter the outer.
Field guide: a scalar subquery returns exactly one value. If it returns more rows than that, SQL will complain. The outer query uses the value as if you'd typed it directly — WHERE height > 20.78.

Page 5 · Comparison ops with subqueries

All six operators work

the value on the right just happens to come from a query

Any place you'd put a literal — 20 or 'pink' — you can put a subquery in parentheses, as long as it returns one value. All the comparison operators you learned for WHERE work unchanged: =, !=, <, >, <=, >=.

subquery.sql
the bed
= scalar
The shortest pink flower.
Try: any combination of comparison op and aggregate. The inner subquery can have its own WHERE — that's how you compare against, say, "the average of just the purple ones."

Page 6 · IN (subquery)

A list, not a single value

IN works just as well with a query result as with a typed list

If the inner basket comes back with several values instead of one, = won't work. Use IN instead — it asks "is this value any of those?"

For example: which flowers share a color with at least two others? The inner query gathers the names of those popular colors (groups of three or more); the outer keeps flowers whose color is in that list.

subquery.sql
the bed
IN subquery
Flowers from the popular colors.
Field guide: IN (subquery) is one of the most common SQL patterns. Many analytical questions become "filter the main table by membership in a derived list."

Page 7 · NOT IN (subquery)

The opposite question

"not in this list"

Sometimes you want the complement — flowers that are not in some derived list. Add NOT before IN:

Below: flowers whose height doesn't match any per-color maximum. The inner query finds the max height per color; the outer keeps flowers whose height isn't in that set of maxes.

An honest caveat: this almost answers "not the tallest in its own color group," but not quite. A flower whose height happens to equal another color's max would also be excluded. It works here only because no heights collide across colors in this bed. A truly correct "not the tallest in its group" test needs a correlated subquery — see Page 10.

subquery.sql
the bed
NOT IN
Every flower that isn't the tallest of its color.
Watch out: NOT IN behaves strangely if the subquery can return NULL — any comparison with NULL produces NULL (not true), so NOT IN tends to return zero rows when even one NULL is in the list. NOT EXISTS is often safer.

Page 8 · EXISTS

Just yes or no

EXISTS asks "did the subquery find anything?"

Sometimes you don't care what the subquery returns — only whether it returns any rows at all. EXISTS turns the subquery into a yes/no answer.

The simplest case is uncorrelated: the inner query doesn't mention the outer row. Below: keep every flower, but only if there's at least one pink in the bed. The inner asks "does any pink flower exist?"; if yes, EXISTS is true for every outer row and all ten are kept. If not, zero come back. An uncorrelated EXISTS acts as a table-wide guard — trivially the same answer for each row.

EXISTS really earns its keep when correlated (Page 10) — the inner then asks a fresh question per outer row, and the yes/no varies row by row.

subquery.sql
the bed
EXISTS
If any pink exists, every flower is kept.
Field guide: EXISTS stops as soon as it finds one row — it doesn't matter what's in that row, only that it's there. The SELECT 1 is a convention: pick anything cheap, since the value won't be used.

Page 9 · Subquery in SELECT

A computed column

a subquery as one of the columns of the result

Subqueries don't only live in WHERE. You can put one in the SELECT list, where it becomes a column of the result. The inner query runs and contributes a value to every row.

Below: each flower's height alongside the bed's overall average — visible side-by-side. The average is computed once and stamped onto each row.

subquery.sql
the bed
subquery in SELECT
Every flower carries the bed average alongside its own height.
Notice: the subquery in SELECT must return exactly one value (like a scalar in WHERE). AS bed_avg gives the computed column a name — without it the column header would be a generic placeholder (this engine shows subquery; real databases often show the whole subquery text).

Page 10 · Correlated subqueries

The inner basket sees the outer row

a different basket per row

So far the inner basket has been the same for every outer row. A correlated subquery is different: it references a column from the outer query, so the smaller basket is recomputed for each outer row.

Below: each flower's height alongside the average of its own color. The inner query has WHERE color = r.color — the r is an alias for the outer row, and the inner sees it. So pink flowers get the pink average, purples get the purple average, and so on.

subquery.sql
the bed
correlated
Each flower paired with the average of its own color.
Field guide: the r in FROM flower_bed r is a table alias. Inside the subquery you write r.color to mean "the color of the outer row I'm being run for." Correlated subqueries are slower (they run once per outer row) but very expressive.

Page 11 · Subqueries vs JOINs

Two ways to ask the same thing

often interchangeable, sometimes only one fits

A lot of subquery questions can also be expressed as JOINs (Chapter IV). Same answer, different shape. Compare:

-- subquery
SELECT species FROM flower_bed
WHERE color IN (
  SELECT color FROM flower_bed
  GROUP BY color HAVING COUNT(*) >= 3
);

-- same answer, with JOIN
SELECT r.species
FROM flower_bed r
INNER JOIN (
  SELECT color FROM flower_bed
  GROUP BY color HAVING COUNT(*) >= 3
) AS popular ON r.color = popular.color;
Same result, different mental model. In real SQL this JOIN-with-subquery pattern is common; this chapter's practice engine doesn't parse it — try it in a full SQL environment when you're ready.

Loose rules of thumb:

  • Use JOINs when you want columns from both tables in the result.
  • Use subqueries when you only want columns from the outer table, filtered by the inner result.
  • Use EXISTS when you only need a yes/no decision per outer row.
  • Use correlated subqueries when each row needs a computed value derived from its own group.

Most experienced gardeners reach for whichever feels clearer for the question at hand.

Page 12 · Anatomy

Where a subquery can live

three positions, plus one twist

in WHERE
with =, <, > (single value) or IN (list) or EXISTS (yes/no)
in SELECT
as a computed column — must return a single value per outer row
in FROM
as a derived table — wrap a SELECT in parens and treat its result like any other table (common in real SQL; this chapter's practice engine doesn't parse it — try it in a full SQL environment when you're ready)
correlated
the inner query references a column from the outer row (an alias is required on the outer table)

One more useful shape worth knowing about: Common Table Expressions (CTEs), written with WITH name AS (SELECT ...) SELECT .... They give a subquery a name you can reuse and read top-to-bottom — useful when one query depends on several intermediate baskets. (Many SQL dialects support them; we won't go deeper here.)

Page 13 · Compose your own

Compose your own subqueries

scalar, IN, EXISTS, correlated — any shape

The flower_bed is yours again. Combine subqueries, aggregates, and filters however you like. The result panel below shows the rows.

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

flower_bed — schema

idinteger
speciestext · Marigold, Tulip, Lotus, Jasmine
colortext · pink, purple, white, orange
heightinteger · cm (nullable)

Page 14 · End of Chapter VI

A basket within a basket

what you've learned, and what's next

In the garden of the Basket Inside the Basket, a smaller basket is gathered first, then used inside a larger one. The smaller basket sometimes holds a single value, sometimes a list, sometimes only the answer to yes or no. Whatever it holds, the larger basket asks of it before deciding what to keep.

scalar subquery
one value, used with =, <, > etc.
IN / NOT IN
membership in a list returned by the subquery
EXISTS / NOT EXISTS
does the subquery have any rows?
in SELECT
computed column derived from a query
correlated
inner sees the outer row — one computation per outer row

After this garden, two more remain in Autumn. The first is called the Directory at the Gate: there the gardener finds any flower without walking every bed. The second is called Looking at Neighbors: each flower, standing in its own row, learns to see how it compares to the ones beside it without losing itself. Beyond Autumn lies Winter, where risky changes are wrapped in a TRANSACTION that can be undone. For today, the smaller basket is set down; the larger one comes home heavier than before.

Check your basket

Two questions to confirm subqueries click. Write a query and press Check.

1. Find every flower taller than the average. Use a scalar subquery. Return species and height.

SELECT species, height FROM flower_bed WHERE height > (SELECT AVG(height) FROM flower_bed);

2. Find every flower whose color group has a tallest member taller than 25 cm. Use IN (subquery). Return species and color.

SELECT species, color FROM flower_bed WHERE color IN ( SELECT color FROM flower_bed GROUP BY color HAVING MAX(height) > 25 );

How did this chapter land?

Your verdict
· Sunset ·
❋
end of Chapter
VI
the smaller basket is set down,
the larger one comes home heavier than before
❋
an intermission is recommended · Chapter VII awaits