SQL Correlated Subqueries
A correlated subquery refers to a column of the outer query, so it cannot run on its own: conceptually it is re-run for every outer row. That makes it the natural way to ask row-by-row questions such as “is this employee the best paid in their own department?” and “what is this customer’s latest order?”.
- Recognise a correlated subquery
- Trace how it runs row by row
- Solve “greatest per group” problems
- Rewrite with joins or window functions for speed
Sample data
| emp_id | first_name | dept_id | salary |
|---|---|---|---|
| 100 | Meera | NULL | 250000 |
| 101 | Asha | 1 | 125000 |
| 102 | Rahul | 1 | 98000 |
| 103 | Priya | 1 | 72000 |
| 104 | Karan | 1 | 72000 |
| 105 | Vikram | 2 | 105000 |
| 106 | Neha | 2 | 54000 |
| 107 | Arjun | 2 | 48000 |
| 108 | Kavya | 3 | 88000 |
| 109 | Rohan | 3 | 45000 |
| 110 | Sneha | 4 | 78000 |
| 111 | Amit | 1 | 25000 |
Correlated vs non-correlated
| Non-correlated | Correlated | |
|---|---|---|
| Refers to the outer query | no | yes |
| Can run on its own | yes | no |
| Runs | once | conceptually once per outer row |
Above their department’s average
- MySQL takes the first outer row, say Asha (dept 1, 125,000).
- The inner query runs with
e.dept_id = 1: the Engineering average is 78,400. - 125,000 > 78,400, so Asha is kept.
- Next row, Priya (dept 1, 72,000): same average, 72,000 is not higher, so she is dropped.
- This repeats for every employee, each compared with their own department. The CEO’s department is NULL, so
x.dept_id = NULLmatches nothing, the average is NULL and she is dropped.
Greatest per group
The top earner in each department:
- For each employee, the subquery finds the maximum salary of their department.
- The employee is kept only if they earn exactly that maximum.
- If two people tied for the top, both would be returned.
Latest order of each customer
Correlated subquery in SELECT
Number of orders next to each customer:
- For each customer row, the subquery counts that customer’s orders.
- Customers without orders get 0 (
COUNTof an empty set), unlike an inner join, which would drop them.
Correlated UPDATE
A correlated subquery can also supply the new value in an UPDATE. Copy the products into a product_sales table, then fill in each product’s total units ordered:
Performance and alternatives
Running a query once per row sounds slow, and on large tables it can be. MySQL 8’s optimizer often rewrites correlated subqueries into joins automatically, and an index on the correlated column (dept_id, customer_id) helps a lot. Two common rewrites:
Join to a pre-aggregated derived table:
Or a window function, covered in Top N per Group.
Exercise: Most expensive product per category
List the most expensive product of each category using a correlated subquery.
| category | product_name | price |
|---|---|---|
| Electronics | Mechanical Keyboard | 3499 |
| Furniture | Standing Desk | 18999 |
| Stationery | Notebook Pack | 249 |
Hint 1
Compare price with the MAX(price) of the same category.
Quick quiz
1. What makes a subquery correlated?
The outer reference ties it to each outer row.
2. Can a correlated subquery be run on its own?
Without the outer query, e.dept_id is unknown.
3. What usually makes correlated subqueries fast on big tables?
The repeated inner lookup becomes an index lookup.
Correlated subquery FAQ
What is a correlated subquery?
A subquery that uses values from the outer query, so it is evaluated for each row of the outer query.
What is the difference between a correlated and a nested subquery?
A plain nested subquery is independent and runs once. A correlated subquery depends on the outer row and conceptually runs once per row.
Are correlated subqueries slow?
They can be on large tables without indexes. MySQL often optimizes them into joins; window functions or joins to aggregated derived tables are common faster alternatives.