LearnFrenzy
Sign in Take a free test

Subqueries and Set Operations · Lesson 17 of 30

Correlated Subqueries and EXISTS

5 min read Updated Nov 7, 2026 Saurabh Samir

The subqueries in the last lesson ran once. A correlated subquery refers to the outer query's current row, so it's evaluated for each row – "employees who earn more than the average of their own department". Correlated subqueries power EXISTS and NOT EXISTS, the safest way to ask "is there a related row?".

In this lesson

  • How correlated subqueries work
  • EXISTS and NOT EXISTS
  • The NOT IN + NULL trap
  • Choosing between subqueries and joins

Correlated subqueries

-- Employees paid above the average of their own department
SELECT e.name, e.department, e.salary
FROM employees e
WHERE e.salary > (
  SELECT AVG(x.salary)
  FROM employees x
  WHERE x.department = e.department      -- refers to the outer row
)
ORDER BY e.department, e.salary DESC;

Output

+-------------+-------------+--------+
| name        | department  | salary |
+-------------+-------------+--------+
| Rohit Mehta | Engineering | 180000 |
| Kavya Reddy | HR          |  90000 |
| Farhan Ali  | Sales       | 140000 |
+-------------+-------------+--------+

Evaluated row by row

  1. Outer row: Rohit, Engineeringinner query: AVG of Engineering = 128,750
  2. 180,000 > 128,750?yes → keep Rohit
  3. Outer row: Sana, Engineering120,000 > 128,750? no → skip
  4. …and so on for every employeethe inner query re-runs with each department

Management has one person, whose salary equals the department average, so no one there is "above average".

The latest order per customer

SELECT o.customer_id, o.order_id, o.order_date
FROM orders o
WHERE o.order_date = (
  SELECT MAX(o2.order_date) FROM orders o2 WHERE o2.customer_id = o.customer_id
)
ORDER BY o.customer_id;

Output

+-------------+----------+------------+
| customer_id | order_id | order_date |
+-------------+----------+------------+
|           1 |      109 | 2025-10-21 |
|           2 |      107 | 2025-10-03 |
|           3 |      112 | 2025-11-06 |
|           4 |      111 | 2025-11-05 |
|           5 |      106 | 2025-09-15 |
|           6 |      108 | 2025-10-10 |
|           7 |      110 | 2025-11-02 |
+-------------+----------+------------+

"Greatest row per group" is a very common task; window functions (chapter 7) solve it too, often more efficiently.

EXISTS and NOT EXISTS

EXISTS (subquery) is true if the subquery returns at least one row. It doesn't matter what it selects – SELECT 1 is the convention – and the database can stop at the first match.

-- Categories that have at least one product in stock
SELECT cat.name
FROM categories cat
WHERE EXISTS (
  SELECT 1 FROM products p
  WHERE p.category_id = cat.category_id AND p.stock > 0
)
ORDER BY cat.name;

Output

+-------------+
| name        |
+-------------+
| Books       |
| Electronics |
| Stationery  |
+-------------+
-- Customers who have never placed an order
SELECT c.name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);

Output

+------------+
| name       |
+------------+
| Vikram Das |
+------------+

The NOT IN + NULL trap

Which categories have no products? This looks right but returns nothing:

SELECT name FROM categories
WHERE category_id NOT IN (SELECT category_id FROM products);

Output

Empty set

The subquery's list contains a NULL (the Desk Lamp). 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. NOT EXISTS doesn't have this problem:

SELECT cat.name FROM categories cat
WHERE NOT EXISTS (SELECT 1 FROM products p WHERE p.category_id = cat.category_id);

Output

+---------+
| name    |
+---------+
| Fitness |
+---------+

Interview angle

"NOT IN vs NOT EXISTS" is a classic question. The answer: they differ when the subquery can return NULL – NOT IN then returns no rows. Prefer NOT EXISTS (or LEFT JOIN … IS NULL), or add WHERE category_id IS NOT NULL inside the NOT IN subquery.

Subquery or join?

QuestionGood fit
Need columns from both tables in the resultJOIN
"Rows that have a match" (no duplicates wanted)EXISTS / IN
"Rows that have no match"NOT EXISTS / LEFT JOIN … IS NULL
Compare with an aggregatescalar or correlated subquery, or a window function

Modern optimisers often turn IN/EXISTS into joins internally, so choose the form that states your intent most clearly – then check the plan with EXPLAIN (chapter 9) if speed matters.

-- JOIN repeats customers once per order; EXISTS lists each customer once
SELECT c.name FROM customers c JOIN orders o ON o.customer_id = c.customer_id WHERE c.city = 'Pune';
SELECT c.name FROM customers c WHERE c.city = 'Pune'
  AND EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);

Output

+------------+
| name       |
+------------+
| Asha Verma |
| Asha Verma |
| Asha Verma |
+------------+
+------------+
| name       |
+------------+
| Asha Verma |
+------------+

Practice

Try it yourself

  1. Find products whose price is the highest in their category.

    Show solution
    SELECT p.name, p.category_id, p.price
    FROM products p
    WHERE p.price = (SELECT MAX(p2.price) FROM products p2 WHERE p2.category_id = p.category_id)
    ORDER BY p.category_id;

    Output

    +---------------------+-------------+---------+
    | name                | category_id | price   |
    +---------------------+-------------+---------+
    | Mechanical Keyboard |           1 | 2499.00 |
    | JavaScript Guide    |           2 |  550.00 |
    | Gel Pen Pack        |           3 |  120.00 |
    +---------------------+-------------+---------+
  2. Find customers who have a delivered order but no pending or shipped orders.

    Show solution
    SELECT c.name
    FROM customers c
    WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.status = 'delivered')
      AND NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.status IN ('pending', 'shipped'))
    ORDER BY c.name;

    Output

    +------------+
    | name       |
    +------------+
    | Meera Nair |
    | Neha Gupta |
    | Ravi Kumar |
    +------------+

Quick check

What makes a subquery "correlated"?

It references a column of the outer query, so it's evaluated per outer row.

What does EXISTS check?

Whether the subquery returns at least one row.

Why can NOT IN return no rows?

If the list contains NULL, every "x <> NULL" is UNKNOWN, so the condition is never TRUE.

Summary

  • Correlated subqueries reference the outer row and run per row – great for per-group comparisons.
  • EXISTS / NOT EXISTS test for related rows and stop at the first match.
  • NOT IN fails silently with NULLs; prefer NOT EXISTS.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular