Skip to content
intermediatePhase 22 · SQL Subqueries

Correlated Subqueries

Write subqueries that reference outer query columns.

45m
3 problems
Topic Progress0%

Correlated Subqueries

Correlated Subqueries

A correlated subquery references a column from the outer query. Unlike a regular subquery that executes once, a correlated subquery executes once for each row processed by the outer query. This makes it powerful but potentially slow on large datasets.

-- Find employees who earn more than the average salary in their department
SELECT employee_name, department_id, salary
FROM employees e1
WHERE salary > (
    SELECT AVG(salary)
    FROM employees e2
    WHERE e2.department_id = e1.department_id
);

In this example, e1.department_id references the outer query. The subquery recalculates the average for each employee's department as the outer query processes each row.

-- Find the most recent order for each customer
SELECT customer_id, order_id, order_date, total_amount
FROM orders o1
WHERE order_date = (
    SELECT MAX(order_date)
    FROM orders o2
    WHERE o2.customer_id = o1.customer_id
);

Correlated subqueries are essential when the subquery logic depends on the current row of the outer query. They cannot be executed independently, which is the key distinction from non-correlated subqueries. Understanding this distinction is critical for query optimization and debugging.

EXISTS with Correlated Subqueries

EXISTS with Correlated Subqueries

The EXISTS operator is the most common way to use correlated subqueries. It returns TRUE if the subquery returns any rows, without examining the actual data. The database can stop searching as soon as it finds the first matching row.

-- Find customers who have placed at least one order over $500
SELECT customer_id, customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
    AND o.total_amount > 500
);

The subquery correlates with c.customer_id from the outer query. The SELECT 1 is a convention; you could select any column since EXISTS only checks for row existence.

-- Find products that have been reviewed by at least one premium customer
SELECT p.product_id, p.product_name
FROM products p
WHERE EXISTS (
    SELECT 1
    FROM reviews r
    JOIN customers c ON r.customer_id = c.customer_id
    WHERE r.product_id = p.product_id
    AND c.customer_type = 'premium'
);

EXISTS is generally more efficient than IN for correlated subqueries because the database optimizer can short-circuit evaluation. Use EXISTS when you need to check for the existence of related rows rather than matching specific values.

Performance Implications

Performance Implications

Correlated subqueries can be performance killers because they execute once per outer row. If the outer query returns 10,000 rows, the subquery executes 10,000 times. This is known as the N+1 problem.

-- Slow: correlated subquery in SELECT
SELECT 
    order_id,
    customer_id,
    (
        SELECT COUNT(*)
        FROM orders o2
        WHERE o2.customer_id = o1.customer_id
    ) AS total_orders
FROM orders o1;

The equivalent JOIN-based approach is often much faster:

-- Faster: using a JOIN
SELECT 
    o1.order_id,
    o1.customer_id,
    c.total_orders
FROM orders o1
JOIN (
    SELECT customer_id, COUNT(*) AS total_orders
    FROM orders
    GROUP BY customer_id
) c ON o1.customer_id = c.customer_id;

To improve correlated subquery performance:

  1. Ensure indexes exist on the columns used in the correlation
  2. Consider rewriting as a JOIN or window function
  3. Use EXISTS instead of IN for existence checks
  4. Limit the outer query result set when possible
-- Add an index to speed up the correlation
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

Window functions like COUNT(*) OVER (PARTITION BY customer_id) are typically the fastest alternative to correlated subqueries in SELECT clauses.

Correlated vs Non-Correlated

Correlated vs Non-Correlated Subqueries

The key difference is independence. A non-correlated subquery can run independently and produces a result that the outer query uses. A correlated subquery depends on the outer query's current row.

-- Non-correlated: subquery runs once
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- Correlated: subquery runs per row
SELECT * FROM products p1
WHERE price > (SELECT AVG(price) FROM products p2 WHERE p2.category = p1.category);

Non-correlated subqueries are generally optimized better because the database can execute them once and cache the result. Correlated subqueries cannot be cached since the result changes for each outer row.

Feature Non-Correlated Correlated
Execution Once Once per outer row
Independence Can run alone Depends on outer query
Optimization Easily cached Harder to optimize
Use case Lookup, aggregation Row-by-row comparison

When you find a correlated subquery, always ask: can this be rewritten as a JOIN or window function? In most cases, the answer is yes, and the performance improvement is significant.

Practice Problems

0/3solved
Employees Above Department Average

Write a query to find all employees who earn more than the average salary in their own department. Return employee_name, department_id, and salary.

Solution
SELECT employee_name, department_id, salary
FROM employees e1
WHERE salary > (
    SELECT AVG(salary)
    FROM employees e2
    WHERE e2.department_id = e1.department_id
);
Latest Order per Customer

Write a query to find the most recent order for each customer. Return customer_id, order_id, order_date, and total_amount.

Solution
SELECT customer_id, order_id, order_date, total_amount
FROM orders o1
WHERE order_date = (
    SELECT MAX(order_date)
    FROM orders o2
    WHERE o2.customer_id = o1.customer_id
);
Products with Above-Category Average Rating

Write a query to find products whose average rating is above the average rating of all products in the same category. Return product_id, product_name, and average_rating.

Solution
SELECT p.product_id, p.product_name, pr.avg_rating
FROM products p
JOIN (
    SELECT product_id, AVG(rating) AS avg_rating
    FROM reviews
    GROUP BY product_id
) pr ON p.product_id = pr.product_id
WHERE pr.avg_rating > (
    SELECT AVG(r2.rating)
    FROM reviews r2
    JOIN products p2 ON r2.product_id = p2.product_id
    WHERE p2.category = p.category
);

Quiz

1. What makes a subquery 'correlated'?

Question 1 options

2. How many times does a correlated subquery execute?

Question 2 options

3. Which is generally faster for existence checks?

Question 3 options

4. What is the primary purpose of SQL Correlated Subqueries?

Question 4 options

Flashcards

Question

What is a correlated subquery?

Answer

A subquery that references a column from the outer query, making it execute once per outer row instead of once.

Question

Why are correlated subqueries slower than non-correlated ones?

Answer

They execute once per outer row (N+1 problem), whereas non-correlated subqueries execute only once and can cache results.

Question

What can you use instead of a correlated subquery in SELECT?

Answer

Window functions (e.g., COUNT(*) OVER (PARTITION BY col)) or JOINs with aggregated derived tables.

Question

What is SQL Correlated Subqueries?

Answer

SQL Correlated Subqueries is a key concept in SQL databases.

Question

When to use SQL Correlated Subqueries?

Answer

Use SQL Correlated Subqueries when building production systems that require reliability, scalability, and maintainability.

Revision Notes

Key Takeaways

  • 1.Correlated subqueries reference the outer query and execute per row
  • 2.EXISTS is preferred for correlated existence checks
  • 3.Consider rewriting as JOINs or window functions for performance
  • 4.Always index columns used in correlations

Interview Tips

  • Explain the N+1 query problem caused by correlated subqueries
  • Show how to rewrite a correlated subquery as a JOIN
  • Discuss when EXISTS is better than IN
  • Mention window functions as modern alternatives

Cheat Sheet

Correlated Subqueries Cheat Sheet

Basic Pattern

SELECT * FROM outer o
WHERE col > (
    SELECT AGG(col) FROM inner i
    WHERE i.id = o.id
);

EXISTS Pattern

SELECT * FROM outer o
WHERE EXISTS (
    SELECT 1 FROM inner i WHERE i.id = o.id
);

Rewrite as JOIN

-- Correlated
SELECT o.*, (SELECT COUNT(*) FROM i WHERE i.id = o.id) FROM o;

-- JOIN equivalent
SELECT o.*, c.cnt FROM o JOIN (SELECT id, COUNT(*) AS cnt FROM i GROUP BY id) c ON o.id = c.id;

Key Points

  • Executes once per outer row
  • Always consider JOIN or window function alternatives
  • Index correlation columns for better performance