Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 28 of 62 · 35 min
Correlated subqueries and derived tables
Explanation
Correlated: the inner query depends on the outer row
In topic 1, (SELECT AVG(salary) FROM employees) gave the same number for every row. Now the question is "who earns more than the average of their own department?". The average depends on which employee we are looking at. The subquery must use the outer row's dept_id. A subquery that refers to a column of the outer query is called correlated.
SELECT e.name, e.dept_id, e.salary
FROM employees AS e
WHERE e.salary > (SELECT AVG(e2.salary)
FROM employees AS e2
WHERE e2.dept_id = e.dept_id);
-- Meera Iyer | 1 | 250000 (Engineering average 140000)
-- Pooja Gupta | 2 | 110000 (Sales average 80666.67)
-- Farhan Ali | 3 | 105000 (Finance average 86500)Notice two aliases on the same table, as in a self join. Without them, WHERE dept_id = dept_id would compare the inner row with itself and always be true, giving the company average again.
What about Deepak, whose dept_id is NULL? Inside the subquery, e2.dept_id = NULL matches nothing, the average is NULL, and 45000 > NULL is unknown, so he is not returned. That is reasonable here, but always think about NULL keys.
Latest order per customer
SELECT c.name, o.id, o.order_date
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.order_date = (SELECT MAX(o2.order_date)
FROM orders AS o2
WHERE o2.customer_id = o.customer_id);
-- Asha Verma | 108 | 2025-04-02
-- Ravi Kumar | 106 | 2025-03-15
-- ... one row per customer who orderedDerived tables: a subquery in FROM
A subquery in FROM returns a whole table, which the outer query reads like any other table. It is called a derived table (or an inline view). It needs an alias so the outer query can refer to its columns. MySQL requires the alias ("Every derived table must have its own alias"), and so did PostgreSQL before version 16, so always write one.
Derived tables answer "aggregate of an aggregate" questions. "What is the average amount a customer has spent?" First total per customer, then the average of those totals. You cannot write AVG(SUM(...)) in one step, but you can nest the steps.
SELECT ROUND(AVG(total), 2) AS avg_total
FROM (SELECT o.customer_id, SUM(oi.qty * oi.price) AS total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status <> 'Cancelled'
GROUP BY o.customer_id) AS t;
-- 3461 (totals 4678, 6497, 1770 and 899)You met a derived table in module 5, where aggregating each table first fixed fan-out. Any time you think "first make this small table, then query it", a derived table (or the CTE of the next topic) is the tool.
Correlated subqueries in SELECT
SELECT d.name,
(SELECT COUNT(*) FROM employees AS e WHERE e.dept_id = d.id) AS headcount
FROM departments AS d;
-- Engineering 4, Sales 3, Finance 2, HR 1, Legal 0A note on performance
Logically, a correlated subquery runs once per outer row. On 11 employees that is nothing; on 10 lakh orders it can be slow if the inner query has to scan the whole table each time. Modern planners often rewrite correlated subqueries into joins, and an index on the linking column (module 10) makes each lookup fast. If a query is slow, compare it with a join to a grouped derived table, or with a window function (module 7).
Common mistakes
- Forgetting the inner alias, so the "correlation" compares the inner table with itself. Give the inner copy its own alias (e2) and write e2.dept_id = e.dept_id.
- Leaving out the alias of a derived table. MySQL rejects it; write ) AS t.
- Trying AVG(SUM(x)). Nesting aggregates needs a derived table or a CTE.
- Using a correlated subquery on a huge table with no index on the linking column. Check the query plan or rewrite as a join.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Correlated subqueries and derived tables · 1/7
Now the inner query uses a value from the outer row. That is called a correlated subquery.
Prefer a video by a teacher? Find free videos on “sql correlated subquery derived table subquery in from clause” on YouTube ↗
In real life: Above-average performers in each branch
A retail chain or a bank compares each branch's staff with their own branch, not with the whole country, because a branch in a metro and one in a small town have very different numbers. "Sales above the branch average" is a correlated subquery, and "average branch total" is a derived table. Analysts write such queries for monthly reviews.
SELECT p.name, p.category, p.price
FROM products AS p
WHERE p.price > (SELECT AVG(p2.price) FROM products AS p2
WHERE p2.category = p.category);Practice (0/4 done)
Which employees earn more than the average salary of their own department? Use a correlated subquery. Return name, dept_id and salary.
Show each customer's latest order. Return the customer name, the order id and its order_date. Use a correlated subquery with MAX(order_date).
What is the average amount spent per customer? First total each customer's spending (sum of qty * price, ignoring Cancelled orders) in a derived table, then average those totals. Return one column, avg_total, rounded to 2 decimal places.
For every department, return its name and headcount (the number of employees), using a correlated subquery in the SELECT list (no JOIN, no GROUP BY). Departments with no employees should show 0.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Common table expressions (WITH)Subqueries, CTEs and set operations · same module
- Subqueries, CTEs and window functionsBackend Development · SQL in depth
- OVER and PARTITION BYWindow functions
- Counting and totals: aggregate functionsBackend Development · Databases and SQL basics
- Why joins, and INNER JOINJOINs
- Aggregate functionsAggregation: GROUP BY and HAVING
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What makes a subquery "correlated"?
Answer: It uses a column of the outer query, so its result depends on the current outer row
Its answer changes from row to row.
Inside WHERE salary > (SELECT AVG(salary) FROM employees WHERE dept_id = dept_id), what does dept_id = dept_id compare?
Answer: The inner row with itself, so it is always true (except for NULLs)
Unqualified names refer to the nearest table. Use aliases: e2.dept_id = e.dept_id.
You want the average of each customer's total spending. Which approach works?
Answer: A derived table that sums per customer, then AVG over it
Aggregates cannot be nested directly; make the per-customer table first.
Which database rejects a derived table without an alias?
Answer: MySQL
MySQL says "Every derived table must have its own alias" (PostgreSQL before 16 also required one).
A correlated subquery is slow on a table of 10 lakh rows. What is a good first step?
Answer: Check the plan, index the linking column, or rewrite it as a join to a grouped derived table
In principle it runs once per outer row; an index or a rewrite helps.
In PostgreSQL, a subquery used with = returns 3 rows. What happens?Related
Answer: An error: more than one row returned by a subquery
PostgreSQL, MySQL and SQL Server raise an error; SQLite quietly uses the first row.
From Scalar subqueries
The subquery SELECT dept_id FROM employees returns 1, 2, 3, 4 and one NULL. How many rows does WHERE id NOT IN (that subquery) return from departments (ids 1 to 5)?Related
Answer: 0
id <> NULL is unknown, so the NOT IN is never true for any row.
What does WHERE price > (SELECT AVG(price) FROM products) do?Related
Answer: Keeps products whose price is above the overall average
The subquery returns one number, the average; the outer WHERE compares each price with it.
Two employees have dept_id NULL. After GROUP BY dept_id, where are they?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Subqueries, CTEs and set operations” before its exam: Common table expressions (WITH), Recursive CTEs, UNION, INTERSECT and EXCEPT.
- Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
- Then the next module, Window functions. 62 topics left in the course before the final exam.