Subqueries & correlated subqueries
Scalar, list and table subqueries, IN, EXISTS, ANY/ALL, derived tables and correlated subqueries.
A subquery is a SELECT inside another statement, wrapped in parentheses. Subqueries let you answer questions in steps: "employees who earn more than the average", "products that have never been ordered". MySQL 8 optimises most of them well. Many can be rewritten as joins, and you'll learn when each style fits.
Sample data#
Scalar subqueries: one value#
A subquery that returns a single row and column can be used like a value:
The inner query runs first (average 68,750), then the outer query compares against it. A scalar subquery can also appear in the SELECT list:
If a "scalar" subquery returns more than one row, MySQL raises ERROR 1242: Subquery returns more than 1 row.
IN and NOT IN: a list of values#
The NOT IN trap. Imran's dept_id is NULL, so the subquery below returns a NULL along with the real ids, and the result is empty:
4 NOT IN (1, 2, 3, NULL) means 4 <> 1 AND 4 <> 2 AND 4 <> 3 AND 4 <> NULL, and the last part is unknown, so the whole condition is never true. Fix it with WHERE dept_id IS NOT NULL inside the subquery, or, better, use NOT EXISTS.
EXISTS and NOT EXISTS#
EXISTS (subquery) is true if the subquery returns at least one row. What it selects doesn't matter (SELECT 1 is conventional):
Legal has no employees. NOT EXISTS handles NULLs correctly and is the safest way to write an anti-join. Notice that the subquery mentions d.id from the outer query. That makes it a correlated subquery.
Correlated subqueries#
A correlated subquery refers to the current row of the outer query, so logically it runs once per outer row. Who earns more than the average of their own department?
For Asha, the inner query averages department 1 (82,667), and for Vikram it averages department 2 (69,000). Correlated subqueries in the SELECT list are a readable way to add per-row lookups:
On big tables, correlated subqueries can be slow if MySQL really does run them per row. Check with EXPLAIN (covered later) and consider a join or window function instead.
ANY and ALL#
> ALL (subquery) means greater than every value, and > ANY (subquery) means greater than at least one:
= ANY (...) is the same as IN (...). In practice, > (SELECT MAX(...)) is often clearer than > ALL.
Derived tables: subqueries in FROM#
A subquery in FROM produces a temporary result set, called a derived table, which must have an alias:
Derived tables are perfect for "aggregate first, then join or filter". MySQL 8.0.14+ also supports LATERAL derived tables, which can refer to earlier tables in the same FROM clause. For readability, the next-but-one lesson's CTEs (WITH ...) are usually nicer than deeply nested derived tables.
Row subqueries#
You can compare several columns at once:
Subqueries in UPDATE and DELETE#
MySQL doesn't let you modify a table and select from the same table in a plain subquery (ERROR 1093). Wrapping the subquery in an extra derived table, as above, materialises it first and works around the restriction.
Subquery or join?#
- Use EXISTS / NOT EXISTS for "has / has no matching rows". It's clear, NULL-safe and never duplicates rows.
- Use a join when you need columns from both tables.
- Use a derived table or CTE to aggregate before joining.
- MySQL's optimiser often turns
INsubqueries into semi-joins automatically, so write the version that reads most clearly first, then check performance withEXPLAIN.
What's next#
Next you'll stack result sets on top of each other with UNION, and find overlaps and differences with INTERSECT and EXCEPT.
Check your understanding
Quick quiz
1.What makes a subquery *correlated*?
2.Why can
WHERE id NOT IN (SELECT manager_id FROM staff)return no rows at all?3.What must you add to a subquery used in the FROM clause (a derived table)?
Finished reading?
Mark this lesson complete to track your progress.