Subqueries and query composition
Scalar, list, correlated and derived-table subqueries
The idea in 30 seconds
A subquery is a query inside another query. Where you put it decides what it must return: one value in a comparison, a list after IN, a whole table in FROM. A correlated subquery refers to the outer row and runs, logically, once per row. Composing small queries like this is how most real questions get answered.
Helps to know first: Aggregation and grouping
On this page
Scalar subquery: one value
Products priced above the average price:
SELECT name, price
FROM products
WHERE price > (SELECT avg(price) FROM products)
ORDER BY price DESC;
name | price
-----------------------------+----------
27-inch monitor | 18999.00
Office chair | 14500.00
Noise-cancelling headphones | 12999.00
Portable SSD 1TB | 7999.00
The inner query runs and its single value takes its place in the comparison. A subquery used as a value must return exactly one row and one column. If it returns more, PostgreSQL stops:
SELECT name, price FROM products
WHERE price > (SELECT price FROM products WHERE category = 'Furniture');
ERROR: more than one row returned by a subquery used as an expression
There are two furniture products, so “the furniture price” does not exist. If a scalar subquery returns no rows, there is no error: its value is NULL.
List subquery: IN
After IN, a subquery can return many rows of one column. Orders that contain a furniture product:
SELECT o.id FROM orders o
WHERE o.id IN (
SELECT oi.order_id
FROM order_items oi JOIN products p ON p.id = oi.product_id
WHERE p.category = 'Furniture'
)
ORDER BY o.id;
Orders 105 and 112.
Correlated subquery: once per row
A subquery can refer to the row the outer query is looking at. Then it is correlated, and its result differs for each outer row. Units sold of each product:
SELECT p.name,
(SELECT coalesce(sum(oi.quantity), 0)
FROM order_items oi
WHERE oi.product_id = p.id) AS units_sold
FROM products p
ORDER BY units_sold DESC, p.name
LIMIT 4;
name | units_sold
-----------------+------------
Wireless mouse | 5
27-inch monitor | 3
Laptop stand | 3
Desk lamp | 2
oi.product_id = p.id links the inner query to the current product. EXISTS subqueries from Joins are correlated too. Logically the subquery runs once per outer row; in practice PostgreSQL often finds a cheaper plan, which the query planning topics show how to inspect.
Derived table: a query in FROM
A subquery in FROM acts as a temporary table for the rest of the query. It must be in parentheses and needs an alias. Average order value across non-cancelled orders, computed in two steps:
SELECT round(avg(order_total), 2) AS avg_order_value, count(*) AS orders
FROM (
SELECT oi.order_id, sum(oi.quantity * oi.unit_price) AS order_total
FROM order_items oi JOIN orders o ON o.id = oi.order_id
WHERE o.status <> 'cancelled'
GROUP BY oi.order_id
) AS totals;
avg_order_value | orders
-----------------+--------
13089.64 | 11
The inner query turns items into one total per order; the outer query averages those totals. This “aggregate, then aggregate again” shape is impossible in a single GROUP BY, and it is the main reason derived tables exist. Common table expressions later give the same idea names and a cleaner layout.
Check yourself
What must a subquery used in
WHERE price > ( … )return?Show answer
Exactly one row with one column. More than one row is an error; no rows makes the value NULL.
What makes a subquery correlated?
Show answer
It refers to a column of the outer query, such as
oi.product_id = p.id, so its result depends on the current outer row.Why do you need a derived table to compute the average order value from
order_items?Show answer
The order totals are themselves sums over items. You must first group items into one total per order, then average those totals: two levels of aggregation, which needs a query inside a query.
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: 4.2 Value ExpressionsPrimary· §4.2.11 Scalar Subqueries
A scalar subquery must return one row with one column; more than one row is an error; no rows gives null.
- 2.PostgreSQL documentation: 9.24 Subquery ExpressionsPrimary· §9.24.1 EXISTS, §9.24.2 IN
IN compares a value with each row a subquery returns; subqueries can refer to columns of the outer query.
- 3.PostgreSQL documentation: 7.2 Table ExpressionsPrimary· §7.2.1.3 Subqueries
A subquery in FROM forms a derived table, enclosed in parentheses and usually given an alias.
Go further
What to read next, and why it is worth the time.
- 1.PostgreSQL documentation: 9.24 Subquery ExpressionsPrimary· §9.24
The exact rules for each kind of subquery comparison, including what NULL does to each one.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.