Skip to content
EL
Contents

Subqueries and query composition

Scalar, list, correlated and derived-table subqueries

ConceptFoundation3 min read15 min hands-on#fundamentals#sql

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

  1. 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.

  2. 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.

  3. 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. 1.
    PostgreSQL documentation: 4.2 Value Expressions
    Primary· §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. 2.
    PostgreSQL documentation: 9.24 Subquery Expressions
    Primary· §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. 3.
    PostgreSQL documentation: 7.2 Table Expressions
    Primary· §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. 1.
    PostgreSQL documentation: 9.24 Subquery Expressions
    Primary· §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.