All 40 lessons
Lesson 22 of 40 · Subqueries & CTEs
Correlated Subqueries and EXISTS
By Skillancy Editorial Team
Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.
A correlated subquery uses a value from the outer query, so it runs once for every outer row. That lets you compare each row with its own group, like each product against the average of its category. The EXISTS operator builds on this to test whether related rows exist.
Syntax
-- Correlated subquery
SELECT ...
FROM table_a a
WHERE a.col > (SELECT AVG(b.col) FROM table_b b WHERE b.key = a.key);
-- EXISTS
SELECT ...
FROM table_a a
WHERE EXISTS (SELECT 1 FROM table_b b WHERE b.key = a.key);Correlated subqueries
1. Products priced above their category average
SELECT p.product_name,
p.category_id,
p.price
FROM products p
WHERE p.price > (
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category_id = p.category_id -- linked to the outer row
)
ORDER BY p.category_id;Try it in the SQL CompilerResult:
| product_name | category_id | price |
|---|---|---|
| Redmi Note 13 Pro | 1 | 23999.00 |
| Samsung 43" Smart TV | 1 | 31990.00 |
| Women's Silk Saree | 2 | 8499.00 |
| Prestige Pressure Cooker 5L | 3 | 2199.00 |
| Kumkumadi Face Oil | 4 | 2995.00 |
| Wings of Fire | 5 | 350.00 |
| Cricket Bat (Kashmir Willow) | 6 | 3499.00 |
(7 rows)
For the Redmi phone, the subquery averages Electronics prices only; for the saree, it averages Fashion prices. That's the "correlation".
2. Correlated subquery in SELECT
Each customer's most recent order date:
SELECT c.customer_name,
(SELECT MAX(o.order_date)
FROM orders o
WHERE o.customer_id = c.customer_id) AS last_order_date
FROM customers c
ORDER BY last_order_date DESC NULLS LAST
LIMIT 5;Try it in the SQL CompilerResult:
| customer_name | last_order_date |
|---|---|
| Arjun Patel | 2026-02-10 |
| Sneha Reddy | 2026-01-26 |
| Ananya Iyer | 2026-01-08 |
| Rohan Gupta | 2025-12-20 |
| Meera Kulkarni | 2025-12-03 |
(5 rows)
3. Each customer's latest order
Combine both ideas: keep only orders whose date equals that customer's latest date.
SELECT o.customer_id, o.order_id, o.order_date, o.status
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;Try it in the SQL CompilerResult:
| customer_id | order_id | order_date | status |
|---|---|---|---|
| 1 | 1019 | 2025-11-15 | Delivered |
| 2 | 1016 | 2025-09-25 | Delivered |
| 3 | 1021 | 2025-12-20 | Delivered |
| 4 | 1023 | 2026-01-26 | Pending |
| 5 | 1017 | 2025-10-12 | Delivered |
| 6 | 1022 | 2026-01-08 | Shipped |
| 7 | 1015 | 2025-09-07 | Returned |
| 8 | 1010 | 2025-06-09 | Delivered |
| 9 | 1024 | 2026-02-10 | Pending |
| 10 | 1020 | 2025-12-03 | Shipped |
| 11 | 1018 | 2025-10-28 | Delivered |
(11 rows)
Window functions solve this more efficiently on large tables, but the logic here is easier to read when you're starting out.
EXISTS and NOT EXISTS
EXISTS (subquery) is true if the subquery returns any row. The columns it selects don't matter, so people write SELECT 1.
4. Customers who have placed at least one order
SELECT c.customer_name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
)
ORDER BY c.customer_name
LIMIT 5;Try it in the SQL CompilerResult:
| customer_name |
|---|
| Aarav Sharma |
| Ananya Iyer |
| Arjun Patel |
| Farhan Khan |
| Ishita Das |
(5 rows)
5. Customers with a cancelled or returned order
SELECT c.customer_name, c.city
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.status IN ('Cancelled', 'Returned')
);Try it in the SQL CompilerResult:
| customer_name | city |
|---|---|
| Aarav Sharma | Bengaluru |
| Vikram Singh | Jaipur |
| Karan Mehta | Mumbai |
| Arjun Patel | Ahmedabad |
(4 rows)
6. NOT EXISTS: products never ordered
SELECT p.product_name
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.product_id
);Try it in the SQL CompilerResult:
| product_name |
|---|
| Cricket Bat (Kashmir Willow) |
(1 row)
Why NOT EXISTS beats NOT IN
NOT IN breaks when the subquery contains a NULL. Watch what happens if we look for products whose brand is not used by any Electronics product, and the subquery includes a NULL brand:
SELECT COUNT(*) AS not_in_result
FROM products
WHERE brand NOT IN (SELECT brand FROM products WHERE category_id IN (1, 6));Try it in the SQL CompilerResult:
| not_in_result |
|---|
| 0 |
(1 row)
Zero, because the cricket bat's NULL brand makes every NOT IN comparison unknown. NOT EXISTS gives the right answer:
SELECT COUNT(*) AS not_exists_result
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM products p2
WHERE p2.category_id IN (1, 6) AND p2.brand = p.brand
);Try it in the SQL CompilerResult:
| not_exists_result |
|---|
| 11 |
(1 row)
(It also counts the cricket bat itself, since a NULL brand matches nothing.) Default to NOT EXISTS for "not in another table" checks.
Common mistakes
- Forgetting the correlation. Without
WHERE o.customer_id = c.customer_id,EXISTSchecks whether the orders table has any row, and returns every customer. - Using a correlated subquery on millions of rows when a join to a grouped table or a window function would run once.
Practice question
Find customers who have bought from more than one category. (Hint: a correlated subquery that counts distinct categories per customer.)
Show solution
SELECT c.customer_name
FROM customers c
WHERE (
SELECT COUNT(DISTINCT p.category_id)
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.customer_id = c.customer_id
) > 1
ORDER BY c.customer_name
LIMIT 6;Try it in the SQL CompilerResult:
| customer_name |
|---|
| Aarav Sharma |
| Ananya Iyer |
| Arjun Patel |
| Karan Mehta |
| Meera Kulkarni |
| Priya Nair |
(6 rows)
Key takeaways
- A correlated subquery refers to the outer row and runs per row.
- It's ideal for "compare to my own group" questions.
EXISTStests whether matching rows exist;NOT EXISTStests that none do.- Prefer
NOT EXISTSoverNOT INwhen NULLs are possible.
Frequently asked questions
What is a correlated subquery?
A correlated subquery refers to a column from the outer query, so it is evaluated once for each outer row. For example, comparing each product's price to the average price of its own category.
What is the difference between EXISTS and IN?
IN compares a value with a list returned by the subquery. EXISTS just checks whether the subquery returns at least one row. Results are usually the same, but NOT EXISTS handles NULLs correctly where NOT IN does not.
Are correlated subqueries slow?
They can be on large tables because they run per row, although optimizers often rewrite them. Window functions or a join to a pre-aggregated table are common faster alternatives.
Keep learning SQL
One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.
Prefer a one-page summary? Download the free SQL cheat sheet (PDF)