Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Correlated Subqueries with Outer References

Recommended resources

Hello! Let's continue our journey into advanced SQL querying.

In our last lesson, we explored how EXISTS and NOT EXISTS use correlated subqueries for efficient existence checks. We established that a correlated subquery is special because it depends on the outer query and runs repeatedly—once for each row the outer query processes. This row-by-row execution model is a fundamental concept in SQL, and understanding it is key to mastering complex queries.

Today, we will generalize that concept. Our goal is to write a correlated subquery that references columns from the outer query not just for an existence check, but to calculate a specific value used for filtering. This allows you to ask more sophisticated questions, such as finding records that are above or below the average of their own specific group.

The Logic of Correlated Subqueries

Before we write any code, let's solidify the mental model of how a correlated subquery executes. The previous lesson introduced this, but it's worth visualizing again, as it's the "low-level" logic you're interested in.

Correlated Subqueries Execution Flow
This flowchart from Oracle's documentation perfectly illustrates the row-by-row execution process. The database gets a row from the outer query, uses a value from that row to run the inner query, and then uses the result to decide whether to keep the outer row.

The key steps are:

  1. The database engine fetches a candidate row from the outer query's source table.
  2. It then executes the inner query (the subquery), using the value(s) from the candidate row.
  3. The inner query returns a result (in today's lesson, a single value).
  4. This result is used in the outer query's WHERE clause to evaluate the condition for the candidate row.
  5. This process repeats for every single row in the outer query's table.

This iterative process is what makes correlated subqueries so powerful for dynamic, row-specific comparisons.


The Essential Role of Table Aliases

When writing correlated subqueries, you will almost always be referencing the same table in both the outer query and the inner subquery. To avoid ambiguity, the database needs you to explicitly state which table you're referring to at any given point.

This is accomplished using table aliases. An alias is a temporary, shorthand name you assign to a table within a query. Without them, a correlated subquery is often impossible to write correctly.


A Classic Use Case: Above-Average Performers

Let's tackle a classic problem that perfectly demonstrates the power of a correlated subquery: Find all employees who earn more than the average salary within their own department.

A simple subquery like (SELECT AVG(salary) FROM employees) won't work, as that calculates the average across the entire company. We need the average to be dynamically calculated for each employee's specific department. This is where correlation comes in.

To see how this is done, we'll turn to a video that explains the concept and then provides a clear demonstration of this exact problem.

Subquery in SQL | Correlated Subquery + Complete SQL Subqueries Tutorial

The following video from techTFQ provides an excellent definition of correlated subqueries and walks through the 'department average salary' problem step-by-step.

Watch from 22:03 to 32:08. Pay close attention to: The explanation of why the subquery is 'correlated' or 'dependent' on the outer query. The use of table aliases (e1 and e2) to distinguish between the outer and inner query's reference to the employees table. The step-by-step walkthrough of how the query is executed for a single employee record.

As the video explained, the query structure looks like this:

SELECT
    emp_name,
    department,
    salary
FROM
    employees e1  -- Outer query alias
WHERE
    salary > (
        SELECT AVG(salary)
        FROM employees e2  -- Inner query alias
        WHERE e2.department = e1.department  -- The CORRELATION
    );

Let's break down the logic:

  1. The outer query starts to process the first employee, let's say 'Alice' from the 'HR' department. e1 refers to Alice's row.
  2. The subquery is then executed. The WHERE clause e2.department = e1.department becomes e2.department = 'HR'.
  3. The subquery calculates the average salary for only the 'HR' department and returns that single value (e.g., 55000).
  4. The outer query's WHERE clause compares Alice's salary to this result: WHERE alice_salary > 55000.
  5. The process repeats for the next employee, Bob, also in 'HR'. The subquery runs again, returning 55000.
  6. When the query gets to Charlie in 'IT', the subquery runs with WHERE e2.department = 'IT', calculating a different average.

This dynamic, row-by-row calculation is the essence of a correlated subquery.


Reinforcing with a Textual Walkthrough and Performance Note

To make sure this concept is crystal clear, let's review a short article that breaks down the same example with sample data. This will allow you to trace the execution at your own pace.

Correlated Subqueries in SQL: A Deep Dive

This article provides a concise written explanation and a step-by-step trace of how the query processes each row. It also makes a critical point about performance.

Read the sections 'Difference Between Independent and Correlated Subqueries', 'Example with Sample Data', and 'Why Are Correlated Subqueries Slow?'. Focus on the step-by-step breakdown of how Alice, Bob, Charlie, and David are processed.

As the article highlights, this power comes at a cost. Because the subquery runs for every row of the outer query, correlated subqueries can be slow on large datasets. If a table has 1 million rows, the subquery will be executed 1 million times! Often, a JOIN or a Common Table Expression (CTE) can achieve the same result more efficiently. We will cover those in upcoming modules, but it's a crucial "job-ready" insight to know that just because a query works, it doesn't mean it's the best way to do it.

Test your understanding!

You have a products table with columns: product_id, product_name, category, and price.

Write a query to find the names and prices of all products that are more expensive than the average price of products in their same category.

Show answer
SELECT
    product_name,
    price
FROM
    products p1
WHERE
    price > (
        SELECT AVG(price)
        FROM products p2
        WHERE p2.category = p1.category
    );

Explanation: For each product (p1), the subquery calculates the average price of all products (p2) that share the same category as p1. The outer query then includes the product only if its price is greater than this category-specific average.


Beyond Averages: Using Other Aggregates

The pattern isn't limited to AVG(). You can use any aggregate function (MAX, MIN, COUNT, SUM) to create sophisticated, row-specific filters. For example, you could find:

  • The movie in each genre with the longest runtime (MAX).
  • The product in each category with the lowest price (MIN).
  • Salespeople whose sales are below the average for their region (AVG).

Let's watch a final short video that demonstrates this flexibility.

SQL Server Queries Part 12 - Correlated Subqueries

This video from WiseOwlTutorials shows how to find the film with the longest running time in each country, using MAX() in a correlated subquery. It then quickly shows how to adapt it to use AVG().

Watch from 02:42 to 07:00. Note how the core structure remains the same; only the aggregate function (MAX vs. AVG) and the correlation column change to answer a different question.


Conclusion

You have now learned the general pattern for writing correlated subqueries. This is a significant step up from simple, independent subqueries and enables a whole new class of analytical questions. By understanding the row-by-row execution logic, you've grasped the "why" behind the syntax.

Key Takeaways:

  • A correlated subquery is executed once for every row processed by the outer query.
  • It is linked to the outer query via a condition in its WHERE clause that references a column from the outer query's row.
  • Table aliases are essential for distinguishing between the inner and outer query's tables.
  • This pattern is perfect for comparing a row's value against an aggregate (like AVG, MAX, MIN) calculated for its specific group.
  • While powerful, they can be inefficient on large tables. Always consider if a JOIN or CTE might be a better-performing alternative.

Preview of the Next Lesson:
We've now used correlated subqueries in the WHERE clause for both existence checks (EXISTS) and value comparisons. But their utility doesn't end there. In our next lesson, we will see how a correlated subquery can be placed directly in the SELECT clause to create a calculated column, allowing you to fetch and display related data alongside each row in your main result set.