36 Scalar and IN Subqueries
A subquery is a SELECT written inside another query. There are three common flavors. A scalar subquery returns at most one row and exactly one column and can be used wherever a single value is allowed. An IN subquery tests membership: WHERE col IN (subquery). An EXISTS subquery tests whether the subquery returns any rows (next lesson). Subqueries can be correlated (referencing the outer row) or uncorrelated (independent). PostgreSQL is very good at optimizing them — you can lean on them without much worry.
What you’ll learn
- Scalar subqueries: one row, one column
IN (subquery)and how it tests membershipNOT INand the NULL trapANY/ALLas more general comparators- When to use a subquery vs a join
Example
A scalar subquery must return at most one row. Zero rows is treated as NULL; more than one row raises an error. They are most useful when you want a value that depends on the table itself (max, count, latest).
IN (subquery) is equivalent to = ANY (subquery) — true when the outer value equals at least one row of the subquery. The planner usually turns this into a semi-join, which can be very efficient.
ANY and ALL generalize comparison to a set. x > ALL (s) is true when x is greater than every element of s (including the empty set, where it is vacuously true). x > ANY (s) is true when x is greater than at least one element.
Correlated subqueries reference columns from the outer query (e.g. o.customer_id = c.id above). They run once per outer row conceptually, though the planner often optimizes them into joins.
Run the example
To run (requires fruits and the join lesson tables):
Common pitfalls
- A scalar subquery that returns more than one row raises “more than one row returned by a subquery used as an expression.” Add a
LIMIT 1and a clearORDER BYif you genuinely want “the first.” NOT IN (subquery)is the famous gotcha: if a NULL is present, nonmatching comparisons produce unknown while matching comparisons produce false; the filter keeps no rows. UseNOT EXISTSfor safety.- Correlated subqueries inside
SELECTlists run once per outer row. If the planner cannot optimize, the cost isO(outer * inner). Re-express as aLEFT JOIN+count() ... GROUP BYwhen the subquery is expensive. INon a long, hard-coded list (IN ('a', 'b', ..., thousands)) is fine but inelegant. For dynamic large lists, prefer a join against a temp table orunnest(array).
Many subqueries can be rewritten as joins or CTEs; the choice is mostly about readability. Use subqueries for single value, joins for multiple rows from another table, and CTEs (next major lesson) when you want to name intermediate results.
Find fruits whose id equals the average id (cast appropriately): SELECT * FROM fruits WHERE id = (SELECT avg(id)::integer FROM fruits);. Then rewrite the correlated subquery as a LEFT JOIN + GROUP BY and compare results.
Downloads
SQL file: scalar-and-in-subqueries.sql · All lesson files · Setup guide