Correlated Subqueries: Row-by-Row Comparisons

Concept. A correlated subquery reads a value from the current outer row. A simple logical model on how it works: evaluate the inner query once per outer row (though the optimizer may rewrite it into a join or another equivalent plan.)

Intuition. In "users with a rating above their own average," the current user determines which rows contribute to the inner average.

A basic subquery supplies one global average of 4.2. A correlated subquery supplies an average for the current outer user: Mickey 4.2, Minnie 4.4, and Daffy 3.9.

Figure 1. A basic subquery produces one global value. A correlated subquery uses a value from the current outer row, so Mickey compares to 4.2, Minnie to 4.4, and Daffy to 3.9. This is the logical model; the optimizer may use an equivalent physical plan.


Problem: Above Their Own Average

Which users have at least one rating above their own average?

For each user, collect their ratings and compute their average, ignoring NULL ratings. Keep the user if any of their ratings exceeds that average.

Each user supplies their own comparison value; one global average answers a different question.


Mental Model: A Double For-Loop

Use a correlated subquery when the comparison depends on the current row.

The inner query has its own scope (l2, its copy of Listens), but it reads u.user_id from the outer scope. That cross-reference is the correlation.

Conceptual double-loop pseudocode, not a required execution plan: the outer loop runs once per user u; the inner SELECT AVG runs each pass over Listens l2 WHERE l2.user_id = u.user_id. An arrow links the inner reference u.user_id back to the outer loop variable u, which is the correlation. u is the outer scope (current user), l2 is the inner scope (this query's Listens). This model computes each user's average: Mickey 4.2, Minnie 4.4, Daffy 3.9. The optimizer may choose an equivalent plan.

Figure 2. This double loop is how to trace the result by hand, not necessarily how the database executes it. It walks one user u at a time. The pink link marks the correlation: l2.user_id = u.user_id reads u from the outer scope, so the inner average uses only that user's rows.


Example: Per-User Averages

SELECT DISTINCT u.name
FROM Users u
JOIN Listens l ON l.user_id = u.user_id
WHERE l.rating > (
    SELECT AVG(rating)
    FROM Listens l2
    WHERE l2.user_id = u.user_id
);

User ratings above their average: the correlated condition WHERE l2.user_id = u.user_id chooses each user's rows. Mickey averages 4.2 and has 4.5 above it; Minnie 4.4 with 4.7; Daffy 3.9 with 4.9. Pluto has no ratings and drops. Output: Mickey, Minnie, Daffy.

Figure 3. The outer query joins each listen to its user. For each joined row, the inner query computes that user's average: Mickey gets 4.2, Minnie gets 4.4, and Daffy gets 3.9. The comparison keeps above-average ratings, and DISTINCT returns each surviving user once. Pluto has no listens, so the inner join excludes him before the comparison.


Example: Existence Checks (EXISTS and NOT EXISTS)

Without a reference to the outer row, EXISTS tests the same global condition for every row. Check that the inner WHERE refers to the intended outer alias.

NOT EXISTS returns Mickey and Pluto. Crossed-out ratings fail the inner condition rating > 4.5. Minnie and Daffy each have surviving ratings, so NOT EXISTS excludes them. Pluto has no listens.

Figure 4. Cross out ratings that fail the inner WHERE. NOT EXISTS keeps users with no surviving rating: Mickey and Pluto. EXISTS would return Minnie and Daffy.


Repeated Inner Work

In the direct logical model, 10,000 outer rows can require 10,000 inner evaluations. The optimizer can rewrite the query into an equivalent join that computes every average in one pass: same answer, far less work.

Figure 5. In the direct logical plan, 10,000 outer rows can trigger 10,000 inner evaluations. We cover how databases optimize this in Module 3.


Takeaway

The outer row determines which inner rows contribute to the result. Check the outer-column reference to see what each inner query depends on.