Subqueries and Set Operations · Lesson 17 of 30
Correlated Subqueries and EXISTS
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
EXISTSandNOT 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;
+-------------+-------------+--------+ | name | department | salary | +-------------+-------------+--------+ | Rohit Mehta | Engineering | 180000 | | Kavya Reddy | HR | 90000 | | Farhan Ali | Sales | 140000 | +-------------+-------------+--------+
Evaluated row by row
- Outer row: Rohit, Engineeringinner query: AVG of Engineering = 128,750
- 180,000 > 128,750?yes → keep Rohit
- Outer row: Sana, Engineering120,000 > 128,750? no → skip
- …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;
+-------------+----------+------------+ | 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;
+-------------+ | 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);
+------------+ | 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);
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);
+---------+ | 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?
| Question | Good fit |
|---|---|
| Need columns from both tables in the result | JOIN |
| "Rows that have a match" (no duplicates wanted) | EXISTS / IN |
| "Rows that have no match" | NOT EXISTS / LEFT JOIN … IS NULL |
| Compare with an aggregate | scalar 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);
+------------+ | name | +------------+ | Asha Verma | | Asha Verma | | Asha Verma | +------------+ +------------+ | name | +------------+ | Asha Verma | +------------+
Practice
Try it yourself
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;+---------------------+-------------+---------+ | name | category_id | price | +---------------------+-------------+---------+ | Mechanical Keyboard | 1 | 2499.00 | | JavaScript Guide | 2 | 550.00 | | Gel Pen Pack | 3 | 120.00 | +---------------------+-------------+---------+
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;+------------+ | 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.