Scalar, Correlated and Non-Correlated Subqueries
coreintermediateA scalar subquery returns exactly one value. A non-correlated subquery runs once, independent of the outer query. A correlated subquery references a column from the outer query, so PostgreSQL conceptually re-evaluates it for each outer row.
Think of it as
These are two independent axes, not three flavors of one thing. "Scalar vs multi-row" is about shape (how many values come back). "Correlated vs non-correlated" is about dependency (does it reference the outer query). A subquery can be scalar and non-correlated (a single independent lookup), scalar and correlated (a per-row lookup), or multi-row and either.
What we're doing: Compare a non-correlated scalar subquery (an overall average) against a correlated one (a per-customer count) in the same query.
- 7
- Correlated: o.customer_id = c.id references the outer row c, so this count differs per customer.
- 8
- Non-correlated: no reference to c anywhere — this is the same single number for every row.
name | order_count | overall_avg
------+-------------+------------
Ada | 2 | 60.0000000000000000
Grace | 1 | 60.0000000000000000
(2 rows)Why this works: order_count changes per row because its subquery filters on c.id, a column from the outer query — PostgreSQL must conceptually know which customer it is currently looking at to evaluate it, which is the definition of correlated. overall_avg is identical on every row precisely because nothing inside it references c at all; it is answering a fixed question ("what is the average across all orders") that does not depend on which customer row it happens to sit next to.
Using a multi-row subquery where a scalar is expected
Wrong
Better
What you see: The query runs fine while the orders table happens to hold rows for only one customer, then breaks the moment a second customer places an order — the exact same query, now failing on more data.
Why: = expects exactly one value on each side; a subquery that returns more than one row cannot satisfy that, and PostgreSQL only discovers the violation at execution time based on how many rows actually come back, not at parse time. IN is designed for a multi-row right-hand side and does not carry this fragility, which is why it is the correct choice whenever the subquery's row count is not guaranteed to be exactly one.
- Non-correlated
- no reference to the outer query
- conceptually runs once, result reused
- e.g. (SELECT max(price) FROM products)
- Correlated
- references a column from the outer query
- conceptually re-evaluated per outer row
- e.g. (SELECT count(*) FROM orders o WHERE o.customer_id = c.id)
Two independent axes: shape and dependency
Together
Remember: Scalar vs multi-row is about shape; correlated vs non-correlated is about whether the subquery references the outer row — a subquery can be any combination of the two.
See also: exists and not exists · in vs exists and null

