Subqueries

A subquery is a query inside another query, written in parentheses. It lets you use the result of one question to answer another: products priced above average, customers who ordered a particular item, each department's share of the payroll. Subqueries can return a single value, a list or a whole table.

Scalar subqueries: a single value

SQL
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
Result 5 rows
nameprice
Mechanical Keyboard89
Wireless Earbuds59.99
Linen Throw49
Night Sky Atlas45
Wooden Train Set39.9

The inner query runs first and returns one value (the average price, 36.65); the outer query then uses it like a constant. A subquery used this way must return exactly one row and one column.

Subqueries in the select list

SQL
SELECT name, price,
       round(price - (SELECT AVG(price) FROM products), 2) AS vs_average
FROM products
WHERE category = 'Books';
Result 3 rows
namepricevs_average
The Patient Gardener24.9-11.75
Learning SQL Step by Step32.5-4.15
Night Sky Atlas458.35

The most expensive product

SQL
SELECT name, price
FROM products
WHERE price = (SELECT MAX(price) FROM products);
Result 1 row
nameprice
Mechanical Keyboard89

This is the correct way to get "the row with the maximum value" — unlike SELECT name, MAX(price) (lesson 18), it works in every database and returns all products if several share the top price.

List subqueries with IN

SQL
SELECT name, email
FROM customers
WHERE id IN (
  SELECT customer_id FROM orders WHERE status = 'pending'
);
Result 3 rows
nameemail
Chloé Martinchloe.martin@example.com
George Browngeorge.brown@example.com
Isaac Leviisaac.levi@example.com

The inner query returns a list of customer ids; IN checks membership. Remember the NOT IN and NULL trap from lesson 9.

SQL
SELECT name
FROM products
WHERE id NOT IN (SELECT product_id FROM order_items);
Result 1 row
name
Smart Plug

Table subqueries in FROM

SQL
SELECT category, round(AVG(line_total), 2) AS avg_line_value
FROM (
  SELECT p.category, oi.quantity * oi.unit_price AS line_total
  FROM order_items AS oi
  JOIN products AS p ON p.id = oi.product_id
) AS lines
GROUP BY category
ORDER BY avg_line_value DESC;
Result 4 rows
categoryavg_line_value
Electronics62.24
Books49.94
Home43.21
Toys31.3

A subquery in FROM (a derived table) acts like a temporary table. It must have an alias in most databases. Derived tables let you aggregate in stages — for example, averaging per-customer totals:

SQL
SELECT round(AVG(total), 2) AS avg_spend_per_customer, MAX(total) AS biggest_spender
FROM (
  SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
  FROM orders AS o
  JOIN order_items AS oi ON oi.order_id = o.id
  GROUP BY o.customer_id
) AS per_customer;
Result 1 row
avg_spend_per_customerbiggest_spender
127.11168.98

Comparing to a group's value

SQL
SELECT d.name AS department, e.first_name, e.salary
FROM employees AS e
JOIN departments AS d ON d.id = e.department_id
JOIN (SELECT department_id, MAX(salary) AS top FROM employees GROUP BY department_id) AS m
  ON m.department_id = e.department_id AND m.top = e.salary
ORDER BY d.name;
Result 4 rows
departmentfirst_namesalary
EngineeringMei98000
MarketingDaniel71000
SalesRafael82000
SupportEthan60000

The highest-paid employee in each department: join each employee to their department's maximum. Window functions (lesson 34) offer another way.

Subqueries in other statements

SQL
UPDATE products SET price = price * 0.9
WHERE id IN (SELECT product_id FROM order_items GROUP BY product_id HAVING SUM(quantity) < 2);

Subqueries work in UPDATE, DELETE and INSERT … SELECT statements too (lessons 35–37).

Try it yourself

Find employees who earn more than the average salary of their own department. (Hint: this needs the inner query to refer to the outer row — the subject of the next lesson.)

Try it yourself
Result