Correlated Subqueries
Subqueries that depend on the outer row — department averages, row-by-row evaluation, EXISTS/NOT EXISTS, correlated UPDATE and DELETE in MySQL, indexing, and JOIN or window-function rewrites.
#Introduction
Correlated subqueries are one of the most important advanced SQL concepts for product-based companies and enterprise applications — frequently asked at Amazon, Microsoft, Oracle, Walmart, Goldman Sachs, JPMorgan, Flipkart and similar companies.
By the end you'll understand what correlated subqueries are, how they differ from normal subqueries, row-by-row evaluation, EXISTS/NOT EXISTS, performance considerations and enterprise use cases.
#Employee Table
| Employee ID | Name | Department | Salary |
|---|---|---|---|
| 101 | John | IT | 70000 |
| 102 | David | IT | 90000 |
| 103 | Lisa | HR | 50000 |
| 104 | Alex | HR | 55000 |
| 105 | Mary | Finance | 80000 |
| 106 | Tom | Finance | 85000 |
#Problem: Employees Earning More Than Their Department Average
A company-wide average won't solve this — each employee must be compared with the average of their own department.
SELECT *
FROM employee e
WHERE salary > (
SELECT AVG(salary)
FROM employee
WHERE department = e.department
);The correlation is department = e.department: e.department comes from the outer query, and the inner query calculates the average for the current employee's department.
How It Executes
| Employee | Department average | Comparison | Returned? |
|---|---|---|---|
| John (IT) | 80000 | 70000 > 80000 | ✗ |
| David (IT) | 80000 | 90000 > 80000 | ✓ |
| Lisa (HR) | 52500 | 50000 > 52500 | ✗ |
| Alex (HR) | 52500 | 55000 > 52500 | ✓ |
| Mary (Finance) | 82500 | 80000 > 82500 | ✗ |
| Tom (Finance) | 82500 | 85000 > 82500 | ✓ |
Final Output
| Name | Department | Salary |
|---|---|---|
| David | IT | 90000 |
| Alex | HR | 55000 |
| Tom | Finance | 85000 |
#NOT EXISTS
Customers who have never placed an order:
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
); -- David, MaryNOT EXISTS returns an outer row only when the correlated subquery finds no matching row.
#Real-World Business Examples
-- 1. Employees above their department average
SELECT *
FROM employee e
WHERE salary > (
SELECT AVG(salary) FROM employee WHERE department = e.department
);
-- 2. Customers with orders
SELECT *
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
-- 3. Products never sold
SELECT *
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM sales s WHERE s.product_id = p.product_id);
-- 4. Highest-paid employees in each department
SELECT *
FROM employee e
WHERE salary = (
SELECT MAX(salary) FROM employee WHERE department = e.department
);
-- 5. Latest order per customer
SELECT *
FROM orders o
WHERE order_date = (
SELECT MAX(order_date) FROM orders WHERE customer_id = o.customer_id
);- Example 4 returns all tied employees if several share a department's maximum salary.
- Example 5 can return several orders for a customer placed on the same latest date. If exactly one row per customer is required, use a window function such as
ROW_NUMBER()with a deterministic tie-breaker.
#Performance Considerations
A correlated subquery is logically evaluated using values from the current outer row — e.g. for 1,000 employees, the correlated condition is checked using each employee's department. But modern optimizers may transform correlated subqueries into more efficient plans.
Importance of Indexing
Without appropriate indexes → potentially slow; with them → much faster. Correlation columns that commonly need indexes: department, customer_id, product_id and email.
CREATE INDEX idx_employee_department ON employee (department);
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_sales_product_id ON sales (product_id);For some queries, composite indexes are even better — e.g. for the latest order per customer:
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);#Interview Questions
Q1Employees earning above their department average.
SELECT *
FROM employee e
WHERE salary > (
SELECT AVG(salary) FROM employee WHERE department = e.department
);Q2The highest-paid employee in every department.
SELECT *
FROM employee e
WHERE salary = (
SELECT MAX(salary) FROM employee WHERE department = e.department
);Q3Customers with no orders.
SELECT *
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);Q4The latest order for every customer.
SELECT *
FROM orders o
WHERE order_date = (
SELECT MAX(order_date) FROM orders WHERE customer_id = o.customer_id
);Q5Products never purchased.
SELECT *
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM sales s WHERE s.product_id = p.product_id);#Common Mistakes
Mistake 1 — Forgetting the Correlation
SELECT *
FROM employee
WHERE salary > (SELECT AVG(salary) FROM employee);This compares employees with the company-wide average, not their department average.
Mistake 2 — Missing Table Aliases
WHERE department = department may simply compare the inner column with itself (always true). Write department = e.department — better still, alias both sides:
SELECT *
FROM employee e
WHERE salary > (
SELECT AVG(e2.salary)
FROM employee e2
WHERE e2.department = e.department
);Mistake 3 — Ignoring Performance
Correlated subqueries on very large datasets can be expensive. Ask: can this be rewritten as a JOIN? Would a window function be clearer? Are the correlation columns indexed? What does EXPLAIN show?
#EXISTS vs IN
SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
SELECT * FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);Both often produce the same result. EXISTS is especially natural when you only need to check whether a related row exists, the subquery is correlated, or the inner table has many matching rows. Modern optimizers may generate similar plans for both — inspect the plan rather than assuming one is universally faster.
#NOT EXISTS vs NOT IN
If the NOT IN subquery returns a NULL, it can produce unexpected results (usually no rows) because of three-valued logic. NOT EXISTS is safer:
SELECT *
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);#Mini Project
Using employee (John IT 70000, David IT 90000, Lisa HR 50000, Alex HR 55000, Mary Finance 80000, Tom Finance 85000) and orders (101 → 2026-01-01, 101 → 2026-02-10, 102 → 2026-03-01, 103 → 2026-02-15):
-- 1. Employees earning above their department average → David, Alex, Tom
SELECT *
FROM employee e
WHERE salary > (SELECT AVG(salary) FROM employee WHERE department = e.department);
-- 2. Highest-paid employee in each department → David, Alex, Tom
SELECT *
FROM employee e
WHERE salary = (SELECT MAX(salary) FROM employee WHERE department = e.department);
-- 3. Customers with orders
SELECT *
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
-- 4. Customers without orders
SELECT *
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
-- 5. Latest order for every customer → 101: 2026-02-10, 102: 2026-03-01, 103: 2026-02-15
SELECT *
FROM orders o
WHERE order_date = (SELECT MAX(order_date) FROM orders WHERE customer_id = o.customer_id);
-- 6. Products that have never been sold
SELECT *
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM sales s WHERE s.product_id = p.product_id);#Practice Problems
Easy
- Find employees earning above their department average.
- Find department toppers.
- Find customers with orders.
- Find customers without orders.
- Find products that have been sold.
Medium
- Find employees earning below their department average.
- Find the oldest employee in each department.
- Find customers whose latest order was placed this month.
- Find departments where every employee earns above 60,000.
- Delete duplicate email records while keeping the oldest row.
Interview level
- Compare correlated subqueries and joins.
- Explain when
EXISTSmay outperformIN. - Optimize a correlated subquery using indexing.
- Rewrite a correlated subquery using a derived table and
JOIN. - Solve "top employee per department" using a correlated subquery, a
JOINand a window function.
#Best Practices
- Use table aliases to make correlations explicit — on both the outer and inner references.
- Index columns used in correlated predicates.
- Prefer
EXISTSfor checking the existence of related rows, andNOT EXISTSfor anti-join logic whenNULLs may be involved. - Consider rewriting expensive correlated subqueries as
JOINs, and window functions for analytical and ranking problems. - Remember MySQL's ERROR 1093 for
UPDATE/DELETEstatements that read the same table in a subquery — use a join or derived table. - Use
EXPLAIN/EXPLAIN ANALYZE, and never assume a correlated subquery literally runs once per outer row.
#Enterprise Usage
Payroll calculations, banking transaction validation, fraud detection, HR performance reports, CRM customer analysis, inventory management, Spring Boot reporting APIs, data-quality jobs, duplicate detection, missing-relationship detection, latest-record queries and business-rule validation.
#Key Takeaways
- A correlated subquery references columns from the outer query and can't execute independently.
EXISTSandNOT EXISTSare the most common correlated-subquery patterns.- They're powerful but can become expensive on large datasets — proper indexing is essential.
- Many correlated queries can also be solved with
JOINs or window functions. NOT EXISTSis generally safer thanNOT INwhenNULLs are possible.- Optimizers may transform correlated queries internally — always verify with the actual execution plan.