Skip to content

Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 28 of 62 · 35 min

Correlated subqueries and derived tables

Explanation

A correlated subquery uses a value from the outer row, so its answer is different for each row: "above the average of their own department". A derived table is a subquery in FROM that acts as a temporary table with an alias, which is the way to aggregate an aggregate, such as the average of per-customer totals.

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.

sql
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)
e is the outer row, e2 the inner copy. HR has one person, who equals the average, so nobody there is above it.

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.

Think of it like this: A teacher marking report cards writes, for each student, "above or below the class average". For a student in 10-A she uses 10-A's average; for 10-B, 10-B's. The question is the same, but the average she looks up depends on the student in front of her. That is a correlated subquery.

Latest order per customer

sql
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 ordered
For each order, keep it only if its date is the latest date for that same customer.

Derived 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.

sql
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)
The inner query makes one row per customer; the outer query averages those rows.

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

sql
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 0
A neat way to get counts, zeros included, without a LEFT JOIN and GROUP BY.

A 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

Correlated subqueries and derived tables

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.

sql
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);
Products priced above their own category's average: Basmati Rice, Pressure Cooker, Running Shoes and Bluetooth Earbuds.

Practice (0/4 done)

Task 1 · Code · SQL

Which employees earn more than the average salary of their own department? Use a correlated subquery. Return name, dept_id and salary.

Task 2 · Code · SQL

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).

Task 3 · Code · SQL

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.

Task 4 · Code · SQL

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

Question 1

1.What makes a subquery "correlated"?

Question 2

2.Inside WHERE salary > (SELECT AVG(salary) FROM employees WHERE dept_id = dept_id), what does dept_id = dept_id compare?

Question 3

3.You want the average of each customer's total spending. Which approach works?

Question 4

4.Which database rejects a derived table without an alias?

Question 5

5.A correlated subquery is slow on a table of 10 lakh rows. What is a good first step?

Read more (free)

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.

From IN, EXISTS and NOT EXISTS

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.

From Subqueries, CTEs and window functions

Two employees have dept_id NULL. After GROUP BY dept_id, where are they?Related

Answer: In one NULL group together

All NULLs fall into a single group.

From GROUP BY

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

  1. 3 more topics in “Subqueries, CTEs and set operations” before its exam: Common table expressions (WITH), Recursive CTEs, UNION, INTERSECT and EXCEPT.
  2. Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Window functions. 62 topics left in the course before the final exam.

← Previous: IN, EXISTS and NOT EXISTS