Himanshu SharmaSoftware engineer
Let’s talk

MySQL · Lesson 27 of 50

Subqueries

Use queries inside queries. Learn scalar, list, derived-table and correlated subqueries in SELECT, WHERE and FROM, and decide when to use a join instead.

15 minCombining tablesUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonWrite subqueries in WHERE, SELECT and FROM, explain what a correlated subquery is, and choose between a subquery and a join.

A query inside a query

Sometimes the answer to one question depends on the answer to another. Which products cost more than the average? You first need the average, then compare. A subquery lets you do both in one statement: a complete SELECT in parentheses, used inside another statement.

sql
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
result
+-----------------------------+--------+
| name                        | price  |
+-----------------------------+--------+
| Noise-Cancelling Headphones | 199.99 |
| Mechanical Keyboard         |  89.50 |
| Standing Desk Mat           |  55.00 |
| Building Blocks Set         |  49.99 |
+-----------------------------+--------+

MySQL evaluates the inner query first (the average is 46.21), then runs the outer query with that number. Because the inner query returns exactly one value, it is called a scalar subquery and can be used anywhere a single value fits.

If a scalar subquery returns more than one row, MySQL stops with an error:

sql
SELECT name FROM products WHERE price = (SELECT price FROM products WHERE category_id = 1);
result
ERROR 1242 (21000): Subquery returns more than 1 row

Subqueries that return a list: IN

When the inner query returns a column of values, test membership with IN. Customers who have placed at least one order:

sql
SELECT id, first_name FROM customers WHERE id IN (SELECT customer_id FROM orders) ORDER BY id;
result
+----+------------+
| id | first_name |
+----+------------+
|  1 | Asha       |
|  2 | Ben        |
|  3 | Chloe      |
|  4 | Daniel     |
|  5 | Elena      |
|  6 | Farid      |
|  7 | Grace      |
|  8 | Hiro       |
| 10 | Jack       |
| 11 | Kavya      |
+----+------------+

Products in the two cheapest categories by average price, or products ordered on a particular day, follow the same pattern. Here, products that appear in any order placed in June 2024:

sql
SELECT name FROM products
WHERE id IN (
  SELECT oi.product_id FROM order_items oi JOIN orders o ON o.id = oi.order_id
  WHERE o.order_date >= '2024-06-01' AND o.order_date < '2024-07-01'
)
ORDER BY name;
result
+---------------------+
| name                |
+---------------------+
| Building Blocks Set |
| Mechanical Keyboard |
| SQL Basics Handbook |
| Water Bottle        |
+---------------------+

Be careful with NOT IN. If the inner list can contain NULL, the whole condition returns no rows (see the NULL lesson). Prefer NOT EXISTS, covered in the next lesson.

Subqueries in the SELECT list

A scalar subquery can also compute a column:

sql
SELECT name, price, ROUND(price - (SELECT AVG(price) FROM products), 2) AS difference_from_average
FROM products
WHERE category_id = 5;
result
+---------------------+-------+-------------------------+
| name                | price | difference_from_average |
+---------------------+-------+-------------------------+
| Building Blocks Set | 49.99 |                    3.78 |
| Puzzle 1000 Pieces  | 19.99 |                  -26.22 |
+---------------------+-------+-------------------------+

Derived tables: subqueries in FROM

A subquery in FROM acts as a temporary table. It must have an alias. This is the way to filter or combine on an aggregate result:

sql
SELECT AVG(order_total) AS average_order_value, MAX(order_total) AS biggest_order
FROM (
  SELECT order_id, SUM(quantity * unit_price) AS order_total
  FROM order_items
  GROUP BY order_id
) AS totals;
result
+---------------------+---------------+
| average_order_value | biggest_order |
+---------------------+---------------+
|          102.490500 |        249.97 |
+---------------------+---------------+

The inner query totals every order. The outer query then averages those totals, something you cannot do directly because you cannot nest aggregate functions like AVG(SUM(...)).

Correlated subqueries

So far each inner query ran once, independently. A correlated subquery refers to a column of the outer query, so its result depends on the current outer row. Products that cost more than the average of their own category:

sql
SELECT p.name, p.category_id, p.price
FROM products p
WHERE p.price > (SELECT AVG(p2.price) FROM products p2 WHERE p2.category_id = p.category_id)
ORDER BY p.category_id, p.price DESC;
result
+-----------------------------+-------------+--------+
| name                        | category_id | price  |
+-----------------------------+-------------+--------+
| Noise-Cancelling Headphones |           1 | 199.99 |
| Mechanical Keyboard         |           1 |  89.50 |
| Data Structures Illustrated |           2 |  45.00 |
| Standing Desk Mat           |           3 |  55.00 |
| Yoga Mat                    |           4 |  27.50 |
| Building Blocks Set         |           5 |  49.99 |
+-----------------------------+-------------+--------+

For each product p, the inner query computes the average price of products in that same category. Conceptually the inner query runs once per outer row, though the optimiser often finds a faster way. It is the SQL equivalent of a nested loop.

Another example counts orders per customer in the select list:

sql
SELECT c.first_name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders
FROM customers c
WHERE c.id <= 4;
result
+------------+--------+
| first_name | orders |
+------------+--------+
| Asha       |      4 |
| Ben        |      2 |
| Chloe      |      2 |
| Daniel     |      2 |
+------------+--------+

Subquery or join?

Many problems can be solved either way. "Customers who have ordered" can be an IN subquery, a join with DISTINCT, or an EXISTS. A good guide:

  • Use a join when you need columns from both tables in the result.
  • Use a subquery when you only need to test or calculate something, and the other table's columns are not needed in the result.
  • Use derived tables (or the WITH clause in a later lesson) to break a complicated query into steps.

Since MySQL 5.6, the optimiser rewrites many subqueries into joins automatically, so write whichever is easier to read, then check the speed with EXPLAIN if it matters.

Common mistakes

  • A scalar subquery that returns several rows ("Subquery returns more than 1 row").
  • Forgetting the alias on a derived table, giving "Every derived table must have its own alias".
  • Using NOT IN with a nullable column.
  • Writing a correlated subquery on a huge table without an index on the correlated column, which gets very slow.
  • Updating a table while selecting from it in a subquery. MySQL refuses with "You can't specify target table for update in FROM clause"; wrap the subquery in a derived table to work around it.

Exercise

Write queries that return: (1) products priced above the average price of the Books category, (2) the customers who have a pending order, using IN, and (3) for each category, the name of its most expensive product, using a correlated subquery.

Show one possible answer
sql
SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products WHERE category_id = 2)
ORDER BY price;

SELECT id, first_name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'pending');

SELECT p.category_id, p.name, p.price
FROM products p
WHERE p.price = (SELECT MAX(p2.price) FROM products p2 WHERE p2.category_id = p.category_id)
ORDER BY p.category_id;
result
+-----------------------------+--------+
| name                        | price  |
+-----------------------------+--------+
| USB-C Hub                   |  39.00 |
| Data Structures Illustrated |  45.00 |
| Building Blocks Set         |  49.99 |
| Standing Desk Mat           |  55.00 |
| Mechanical Keyboard         |  89.50 |
| Noise-Cancelling Headphones | 199.99 |
+-----------------------------+--------+
+----+------------+
| id | first_name |
+----+------------+
|  2 | Ben        |
|  6 | Farid      |
|  7 | Grace      |
+----+------------+
+-------------+-----------------------------+--------+
| category_id | name                        | price  |
+-------------+-----------------------------+--------+
|           1 | Noise-Cancelling Headphones | 199.99 |
|           2 | Data Structures Illustrated |  45.00 |
|           3 | Standing Desk Mat           |  55.00 |
|           4 | Yoga Mat                    |  27.50 |
|           5 | Building Blocks Set         |  49.99 |
+-------------+-----------------------------+--------+

Key points

  • A subquery is a SELECT inside another statement, written in parentheses. The inner query runs to supply a value, a list or a table.
  • A scalar subquery returns one value, a list subquery works with IN, and a derived table in FROM must have an alias.
  • A correlated subquery refers to the outer row and is conceptually re-run for each outer row.
  • Many subqueries can be written as joins. Pick whichever is clearer; modern MySQL optimises both well.

Check yourself

1. What does SELECT name FROM products WHERE price > (SELECT AVG(price) FROM products); return?
2. What must you give a subquery used in the FROM clause?
3. What makes a subquery correlated?
4. A scalar subquery returns two rows. What happens?