SQL Tutorial › Subqueries & CTEs › Correlated Subqueries

SQL Correlated Subqueries

AdvancedLesson 41 of 6910 min read6 queries run on MySQL 8.4

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?”.

Advertisement
In this lesson
  • 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

employees · 12 rows (some columns)
emp_idfirst_namedept_idsalary
100MeeraNULL250000
101Asha1125000
102Rahul198000
103Priya172000
104Karan172000
105Vikram2105000
106Neha254000
107Arjun248000
108Kavya388000
109Rohan345000
110Sneha478000
111Amit125000

Correlated vs non-correlated

Non-correlatedCorrelated
Refers to the outer querynoyes
Can run on its ownyesno
Runsonceconceptually once per outer row

Above their department’s average

SQL
SELECT e.first_name, e.dept_id, e.salaryFROM employees eWHERE e.salary > (  SELECT AVG(x.salary)  FROM employees x  WHERE x.dept_id = e.dept_id)ORDER BY e.dept_id, e.salary DESC;
ResultMySQL 8.4
first_namedept_idsalary
Asha1125000
Rahul198000
Vikram2105000
Kavya388000
4 rows in set
How this query works, step by step
  1. MySQL takes the first outer row, say Asha (dept 1, 125,000).
  2. The inner query runs with e.dept_id = 1: the Engineering average is 78,400.
  3. 125,000 > 78,400, so Asha is kept.
  4. Next row, Priya (dept 1, 72,000): same average, 72,000 is not higher, so she is dropped.
  5. This repeats for every employee, each compared with their own department. The CEO’s department is NULL, so x.dept_id = NULL matches nothing, the average is NULL and she is dropped.

Greatest per group

The top earner in each department:

SQL
SELECT e.dept_id, e.first_name, e.salaryFROM employees eWHERE e.salary = (  SELECT MAX(x.salary)  FROM employees x  WHERE x.dept_id = e.dept_id)ORDER BY e.dept_id;
ResultMySQL 8.4
dept_idfirst_namesalary
1Asha125000
2Vikram105000
3Kavya88000
4Sneha78000
4 rows in set
How this query works, step by step
  1. For each employee, the subquery finds the maximum salary of their department.
  2. The employee is kept only if they earn exactly that maximum.
  3. If two people tied for the top, both would be returned.

Latest order of each customer

SQL
SELECT o.customer_id, o.order_id, o.order_date, o.statusFROM orders oWHERE o.order_date = (  SELECT MAX(o2.order_date)  FROM orders o2  WHERE o2.customer_id = o.customer_id)ORDER BY o.customer_id;
ResultMySQL 8.4
customer_idorder_idorder_datestatus
110082025-04-28Pending
210072025-04-10Delivered
310092025-05-15Delivered
410052025-03-08Delivered
510102025-06-01Delivered
5 rows in set

Correlated subquery in SELECT

Number of orders next to each customer:

SQL
SELECT c.customer_name,       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id) AS ordersFROM customers cORDER BY orders DESC, c.customer_name;
ResultMySQL 8.4
customer_nameorders
Ananya Sharma3
Fatima Khan2
Ishita Bose2
Rohit Kumar2
David Fernandes1
Manoj Pillai0
Sara Thomas0
7 rows in set
How this query works, step by step
  1. For each customer row, the subquery counts that customer’s orders.
  2. Customers without orders get 0 (COUNT of 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:

MySQL
CREATE TABLE product_sales AS SELECT product_id, product_name, 0 AS units FROM products;UPDATE product_sales psSET units = (SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.product_id = ps.product_id);SELECT * FROM product_sales ORDER BY units DESC, product_id;
ResultMySQL 8.4
mysql> CREATE TABLE product_sales AS SELECT product_id, product_name, 0 AS un…
Query OK, 9 rows affected
mysql> UPDATE product_sales ps
Query OK, 8 rows affected
mysql> SELECT * FROM product_sales ORDER BY units DESC, product_id
product_idproduct_nameunits
6Notebook Pack18
7Gel Pens6
2Mechanical Keyboard3
4Office Chair3
8Desk Lamp3
1Wireless Mouse2
3USB-C Hub2
5Standing Desk1
9Monitor Arm0
9 rows in set

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:

SQL
SELECT e.dept_id, e.first_name, e.salaryFROM employees eJOIN (SELECT dept_id, MAX(salary) AS max_salary FROM employees GROUP BY dept_id) m  ON m.dept_id = e.dept_id AND m.max_salary = e.salaryORDER BY e.dept_id;
ResultMySQL 8.4
dept_idfirst_namesalary
1Asha125000
2Vikram105000
3Kavya88000
4Sneha78000
4 rows in set

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.

Expected resultMySQL 8.4
categoryproduct_nameprice
ElectronicsMechanical Keyboard3499
FurnitureStanding Desk18999
StationeryNotebook Pack249
3 rows in set
Hint 1

Compare price with the MAX(price) of the same category.

Quick quiz

1. What makes a subquery correlated?

2. Can a correlated subquery be run on its own?

3. What usually makes correlated subqueries fast on big tables?

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.