SQL • Subqueries and CTEs

Correlated Subqueries — Use correlated subqueries (intro)

Learn how inner queries that reference outer rows unlock powerful row-by-row filtering and computation in SQL.

SQL illustration with a database, structured table, and query code symbols
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

Historical Context & Motivation

The relational model, introduced by E.F. Codd in 1970, established a mathematical foundation for querying structured data through relational algebra and relational calculus. Early implementations translated these formalisms into practical query languages, but expressing row-dependent conditions—where a filter for one row depends on data from other rows—remained cumbersome. The correlated subquery emerged as a natural syntactic construct to address this gap, allowing an inner query to reference columns from the outer query and thereby evaluate a condition once per candidate row. This capability transformed SQL from a language of static set operations into one capable of expressing nuanced, row-contextual logic.

1970 • Codd's Relational Model

E.F. Codd publishes "A Relational Model of Data for Large Shared Data Banks," establishing the theoretical groundwork for relational databases and declarative query languages.

1974 • SEQUEL at IBM

Chamberlin and Boyce design SEQUEL (later renamed SQL), introducing subquery support in SELECT, WHERE, and HAVING clauses as part of System R.

1986 • SQL-86 Standard

ANSI ratifies the first SQL standard, formally codifying correlated subqueries as part of the language specification, enabling portable use across database vendors.

1999 • SQL:1999 & Scalar Subqueries

The SQL:1999 standard expands subquery placement to include the SELECT list and FROM clause (lateral joins), broadening the applicability of correlated patterns.

2010s • Modern Optimizers

Query optimizers in PostgreSQL, SQL Server, and Oracle learn to decorrelate subqueries automatically, converting them into equivalent joins for better performance while preserving the original semantics.

The central question that correlated subqueries answer is: How can we evaluate a condition for each outer row that depends on the data context of that specific row? A non-correlated (or "simple") subquery executes once and produces a single result set that the outer query consumes. In contrast, a correlated subquery re-executes for every row the outer query processes, because it references one or more columns from the outer query. Understanding this distinction is essential for writing expressive SQL and reasoning about query performance.

SECTION 2

Core Principles & Definitions

A correlated subquery is distinguished from a non-correlated subquery by a single defining characteristic: the inner query contains a reference to a column from the outer query's current row. This reference creates a data dependency that forces the database engine to conceptually re-evaluate the subquery for every candidate row in the outer query. While modern optimizers may rewrite this execution model internally, the logical semantics remain row-by-row evaluation. The following principles capture the essential mental model you need to write and reason about correlated subqueries effectively.

1

Outer Reference

The inner subquery references at least one column from the outer query's table. This column acts as a parameter that changes with each outer row, making the subquery's result row-dependent.
2

Row-by-Row Evaluation

Conceptually, the database engine iterates over each outer row, substitutes the current outer column values into the inner query, executes it, and uses the result to filter or compute the outer row's output.
3

Scope & Alias Resolution

SQL resolves column names from the innermost scope outward. Aliasing the outer table is critical to avoid ambiguity when inner and outer tables share column names or are the same table (self-correlation).
4

Result Cardinality

When used in a WHERE clause with operators like =, <, >, the correlated subquery must return a scalar (single value). When used with EXISTS, IN, or ANY/ALL, it may return a set of rows.
5

Non-Correlated Contrast

A non-correlated subquery has no outer reference, executes once, and produces a static result set. It is independent of the outer query's row context, making it simpler but less expressive for row-level comparisons.
✦

KEY TAKEAWAY

Think of a correlated subquery like a function call inside a loop. The outer query is the loop iterating over rows, and for each iteration, the inner subquery is "called" with the current row's data as an argument. Just as a function in a for-loop can produce a different result on each iteration because its parameters change, a correlated subquery produces a different result for each outer row because its outer reference changes. This is fundamentally different from a non-correlated subquery, which behaves like computing a constant before the loop begins.

SECTION 3

Visual Explanation — Execution Flow

The diagram below illustrates the conceptual execution of a correlated subquery. The outer query iterates through its rows one at a time. For each outer row, the correlated subquery receives the current row's referenced column value, executes against its target table, and returns a result. That result is then used in the outer query's WHERE clause (or SELECT list) to determine whether the outer row is included in the final output or what computed value is assigned to it.

Correlated Subquery Execution FlowOUTER QUERY (employees)id name dept_id1 Alice 1012 Bob 1023 Carol 1014 Dave 1035 Eve 102▲ current row: Alice (dept_id=101)pass dept_id=101INNER SUBQUERYSELECT AVG(salary)FROM salaries sWHERE s.dept_id = e.dept_idFilters salaries to dept 101Computes AVG → 72000↑ outer reference (correlation)returns 72000Comparesalary >72000?REPEAT FOR EACH OUTER ROWRow 2 (Bob, dept_id=102) → subquery re-runs with dept_id=102 → new AVGRow 3 (Carol, dept_id=101) → subquery re-runs with dept_id=101 → same AVG as row 1FINAL RESULT SETOnly rows where employee's salary exceeds their department's average salary are returned.The subquery was logically evaluated N times (once per outer row).
The outer query iterates over the employees table row by row. For the highlighted row (Alice, dept_id=101), the correlated subquery receives e.dept_id = 101 as an outer reference, computes the average salary for department 101, and returns 72000. The outer query then compares Alice's salary against this value to decide inclusion in the result set.

Notice the critical element in the diagram: the outer reference (labeled e.dept_id) inside the inner query creates the correlation. Without that reference, the subquery would execute once, compute a global average across all departments, and the query would degenerate into a non-correlated subquery. The outer reference is what makes the subquery context-aware—it sees a different slice of data for each outer row. Also observe that the subquery re-executes for row 3 (Carol) with dept_id=101 and would produce the same result as row 1, which is why optimizers often cache or decorrelate to avoid redundant computation.

SECTION 4

How Correlated Subqueries Work — Syntax & Semantics

Correlated subqueries can appear in three main positions within a SQL statement: the WHERE clause (for filtering), the SELECT list (as a scalar computation per row), and the HAVING clause (for group-level filtering). The general syntactic pattern is the same in each case: the inner query references a column from the outer query's FROM clause via a table alias.

Pattern 1 — Correlated Subquery in WHERE

WHERE CLAUSE PATTERN

SELECTcolumnsFROMtableouteroWHEREo.coloperator(SELECTagg(col)FROMtableinneriWHEREi.fk=o.pk)SELECT columns FROM table_outer o WHERE o.col operator (SELECT agg(col) FROM table_inner i WHERE i.fk = o.pk)

Here o aliases the outer table, i aliases the inner table, and i.fk = o.pk is the correlation predicate that ties the inner query to the current outer row. The operator can be =, <, >, >=, <=, or <>, and the subquery must return a scalar when used with these operators.

Pattern 2 — Correlated Subquery in SELECT (Scalar)

SELECT LIST PATTERN

SELECTo.col,(SELECTagg(col)FROMtableinneriWHEREi.fk=o.pk)AScomputedcolFROMtableouteroSELECT o.col, (SELECT agg(col) FROM table_inner i WHERE i.fk = o.pk) AS computed_col FROM table_outer o

The subquery in the SELECT list must return exactly one value (a scalar) for each outer row. If it returns more than one row, the query engine will raise an error. This pattern is useful for appending a computed column without a JOIN.

Pattern 3 — EXISTS with Correlated Subquery

EXISTS PATTERN

SELECTcolumnsFROMtableouteroWHEREEXISTS(SELECT1FROMtableinneriWHEREi.fk=o.pkANDcondition)SELECT columns FROM table_outer o WHERE EXISTS (SELECT 1 FROM table_inner i WHERE i.fk = o.pk AND condition)

The EXISTS operator tests whether the correlated subquery returns at least one row. It short-circuits: once one matching row is found, it returns TRUE immediately. This makes it efficient for existence checks and is often preferred over IN with subqueries.

⚠

Aliasing Is Non-Negotiable

When the inner and outer queries reference the same table (a self-correlated subquery), you must alias both references distinctly—e.g., employees e1 in the outer query and employees e2 in the inner query. Without distinct aliases, the SQL engine cannot determine which table reference a column belongs to, leading to ambiguous column errors or incorrect results.

SECTION 5

Correlated vs. Non-Correlated — A Side-by-Side Classification

Understanding when to use a correlated subquery versus a non-correlated one depends on whether the inner query's logic is independent of the outer row. The following table and diagram clarify the structural and behavioral differences between these two subquery types, and when each is the appropriate tool.

CharacteristicNon-Correlated SubqueryCorrelated Subquery
Outer reference?No — inner query is self-containedYes — inner query references outer column(s)
Execution frequencyOnce (result cached and reused)Conceptually once per outer row
Evaluation orderInside-out: subquery first, then outerOutside-in: outer row drives inner execution
Typical use caseCompare against a fixed value or fixed setCompare each row against a row-specific aggregate or existence test
Performance concernGenerally efficient (single execution)Can be expensive on large tables without optimization or indexes
Can run independently?Yes — can be tested in isolationNo — depends on outer query context

Structural and behavioral comparison between non-correlated and correlated subqueries

Non-Correlated vs. Correlated SubqueryNON-CORRELATEDInner QueryExecutes ONCEStatic Result{val₁, val₂, ...}Outer QueryUses static result forALL rows at onceExample:WHERE salary > (SELECT AVG(salary) FROM employees)↑ No outer referenceOne global average for everyoneCORRELATEDOuter QueryRow-by-row scanpass row dataInner Query (re-evaluated)Receives outer column valueReturns result specific to this rowreturn & repeatExample:WHERE salary > (SELECT AVG(s.salary) FROM employees s WHERE s.dept=e.dept↑ Outer reference → per-dept average)
Left: a non-correlated subquery computes a single global average and the outer query uses that static value for all rows. Right: a correlated subquery receives e.dept from each outer row, so it computes a department-specific average that changes with each row. The dashed loop on the right represents the repeated execution cycle.

A useful heuristic for determining correlation: if you can copy the subquery, paste it into a new query window, and execute it in isolation, it is non-correlated. If it fails because it references an alias defined only in the outer query, it is correlated. This "standalone test" is the fastest way to classify a subquery in practice.

SECTION 6

Worked Example — Employees Earning Above Department Average

Consider two tables: employees (columns: emp_id, name, dept_id, salary) and departments (columns: dept_id, dept_name). We want to find all employees whose salary exceeds the average salary of their own department. This is a classic correlated subquery scenario because the average must be computed per department, and the relevant department changes for each row.

Find employees earning above their department's average salary

  1. 1

    Step 1 — Write the outer query skeleton

    Start with the outer SELECT that retrieves the desired columns from the employees table. Alias the table as e so the inner query can reference it: SELECT e.name, e.salary, e.dept_id FROM employees e WHERE ...
    SELECT e.name, e.salary, e.dept_id FROM employees e WHERE ...
  2. 2

    Step 2 — Write the correlated subquery

    The subquery computes the average salary for the current outer row's department. We query the same employees table but alias it as e2 to distinguish it from the outer reference. The correlation predicate is e2.dept_id = e.dept_id, which restricts the average to the current outer row's department.
    (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id)
  3. 3

    Step 3 — Combine with comparison operator

    Place the subquery in the WHERE clause with the > operator. The complete query reads: for each employee e, keep the row only if e.salary exceeds the average salary of all employees in the same department.
    SELECT e.name, e.salary, e.dept_id FROM employees e WHERE e.salary > (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id);
  4. 4

    Step 4 — Trace execution for one row

    Suppose the outer query is currently processing Alice (dept_id=101, salary=80000). The subquery substitutes e.dept_id = 101 and computes AVG(salary) for all employees in department 101. If the average is 72000, then 80000 > 72000 is TRUE, so Alice is included in the result. When the outer query advances to Bob (dept_id=102, salary=55000), the subquery re-runs with dept_id=102, yielding a different average.
    Alice: 80000 > 72000 → INCLUDED
  5. 5

    Step 5 — Verify correctness

    To validate the correlated subquery, you can manually compute department averages using a GROUP BY query: SELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id. Then compare each employee's salary against their department's average from this result set. The output should match the correlated subquery's output exactly. This manual check also illustrates that a JOIN-based approach (joining against a derived table of department averages) is an alternative to the correlated subquery.
    Cross-verified: results match a manual GROUP BY comparison.

SECTION 7

Strengths, Limitations & When to Use Correlated Subqueries

Correlated subqueries are powerful but not universally the best tool. Understanding their trade-offs relative to alternative SQL constructs—JOINs, window functions, and CTEs—allows you to make informed design decisions. The table below summarizes the key strengths and limitations.

StrengthsLimitations
Highly expressive: naturally models "for each row, compute something based on related data" logicCan be slow on large tables if the optimizer cannot decorrelate the query
EXISTS pattern is often the most efficient way to test for the existence of related rowsCannot be tested in isolation — harder to debug than non-correlated subqueries
Works in UPDATE and DELETE statements for row-conditional modificationsReadability degrades with deeply nested correlations or multiple outer references
Portable across all SQL-compliant databases (part of ANSI standard)Often replaceable by window functions (e.g., AVG() OVER(PARTITION BY ...)) which may be more readable and efficient
Natural fit for self-referencing queries (e.g., comparing a row to aggregates of its siblings)Scalar subqueries in SELECT must return exactly one row — violating this causes runtime errors

Strengths and limitations of correlated subqueries

✦

WHEN TO REACH FOR A CORRELATED SUBQUERY

Use a correlated subquery when you need row-contextual logic that cannot be expressed with a simple JOIN or when EXISTS provides the cleanest existence check. If you find yourself writing a correlated subquery that computes a window-like aggregate (e.g., running totals, per-group rankings), consider whether a window function would be more performant and readable. Think of correlated subqueries as a scalpel: precise for targeted row-level conditions, but a broader tool like a JOIN or window function may be more ergonomic for aggregate patterns.

SECTION 8

Connection to Advanced SQL — CTEs, LATERAL Joins & Window Functions

Correlated subqueries are the conceptual predecessor to several more modern SQL features. Understanding them deeply prepares you to leverage Common Table Expressions (CTEs), LATERAL joins, and window functions—all of which can express similar logic with different trade-offs in readability, maintainability, and performance. The table below maps each correlated subquery pattern to its modern equivalent.

Correlated Subquery PatternModern AlternativeKey Difference
WHERE col > (SELECT AGG(...) WHERE corr)Window function: AGG(...) OVER(PARTITION BY ...)Window functions compute the aggregate in a single pass; no re-execution per row.
Scalar subquery in SELECTLATERAL JOIN or LEFT JOIN on derived tableLATERAL explicitly allows the derived table to reference outer columns; more readable for multi-column returns.
WHERE EXISTS (SELECT 1 ... WHERE corr)SEMI JOIN via CTE + JOINEXISTS is often already optimal; the CTE version is useful when the same derived set is reused multiple times.
Self-correlated filter (same table inner/outer)Self-JOIN with GROUP BYSelf-JOINs can be easier to read for simple aggregations but require explicit grouping.

Mapping correlated subquery patterns to modern SQL alternatives

As you advance in your SQL studies, you will encounter situations where a correlated subquery is the most natural expression of the business logic, and others where a CTE or window function yields cleaner code. The key insight is that these constructs are semantically equivalent in many cases—they express the same logical operation but with different syntax and potentially different execution plans. Mastering correlated subqueries first gives you the foundational mental model of row-contextual evaluation, which transfers directly to understanding LATERAL joins and the PARTITION BY semantics of window functions.

SECTION 9

Practice Problems

The following problems use two tables: products(product_id, product_name, category_id, price) and orders(order_id, product_id, quantity, order_date). Assume standard foreign key relationships. Work through each problem before reading the answer.

Question 1: CONCEPTUAL

Explain in your own words why the following subquery is correlated and not non-correlated: SELECT p.product_name FROM products p WHERE p.price > (SELECT AVG(p2.price) FROM products p2 WHERE p2.category_id = p.category_id). What would happen if you removed the WHERE clause from the inner query?
Show answer
The subquery is correlated because it references p.category_id from the outer query's products p alias. This outer reference means the inner query's result changes depending on which outer row is being evaluated—specifically, it computes the average price only for products in the same category as the current outer row. If you removed WHERE p2.category_id = p.category_id, the subquery would compute a single global average across all products and become non-correlated. The query would then return products priced above the global average rather than above their category's average—a fundamentally different result.

Question 2: BASIC

Write a correlated subquery to find all products that have been ordered at least once. Use the EXISTS operator. Your query should return product_id and product_name.
Show answer
The solution uses EXISTS with a correlated subquery: SELECT p.product_id, p.product_name FROM products p WHERE EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.product_id). For each product in the outer query, the inner query checks whether at least one order row references that product's product_id. The EXISTS operator short-circuits as soon as it finds one matching row, making this efficient. The SELECT 1 is conventional—EXISTS only checks for row existence, not the values returned.

Question 3: INTERMEDIATE

Write a query using a correlated subquery in the SELECT list to display each product's name, price, and the average price of all products in the same category. Name the computed column category_avg_price.
Show answer
The query places a correlated scalar subquery in the SELECT list: SELECT p.product_name, p.price, (SELECT AVG(p2.price) FROM products p2 WHERE p2.category_id = p.category_id) AS category_avg_price FROM products p. For each outer row, the subquery computes the average price among all products sharing the same category_id. Because AVG always returns exactly one value, the scalar constraint is satisfied. Note that an equivalent and often more efficient approach would use a window function: AVG(price) OVER(PARTITION BY category_id).

Question 4: APPLIED

A product manager wants to find products that have never been ordered in quantities greater than 10 units in a single order. Write a query using NOT EXISTS with a correlated subquery to return these products' names and prices.
Show answer
The query uses NOT EXISTS to find the absence of large orders: SELECT p.product_name, p.price FROM products p WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.product_id AND o.quantity > 10). NOT EXISTS returns TRUE when the correlated subquery yields zero rows. For each product, the inner query searches for any order with that product's ID and a quantity exceeding 10. If no such order exists, the product passes the filter. This includes products that have never been ordered at all (zero orders also means no orders with quantity > 10). If the business requirement is to exclude never-ordered products, an additional EXISTS check would be needed.

Question 5: CRITICAL THINKING

Consider this correlated subquery: SELECT p.product_name, p.price FROM products p WHERE p.price = (SELECT MAX(p2.price) FROM products p2 WHERE p2.category_id = p.category_id). (a) What does this query return? (b) Can this be rewritten using a window function? If so, how? (c) Under what circumstances might the correlated subquery version outperform or underperform the window function version?
Show answer
(a) The query returns the most expensive product(s) in each category. If two products share the max price within a category, both are returned. (b) The window function equivalent is: SELECT product_name, price FROM (SELECT product_name, price, MAX(price) OVER(PARTITION BY category_id) AS max_price FROM products) sub WHERE price = max_price. Alternatively, RANK() OVER(PARTITION BY category_id ORDER BY price DESC) = 1 achieves the same result. (c) The window function version makes a single pass over the data, so it typically outperforms the correlated subquery on large tables without appropriate indexes. However, if there is an index on (category_id, price), the correlated subquery can efficiently seek the MAX for each category, potentially matching or beating the window function. The optimizer's ability to decorrelate the subquery into a join also affects performance—modern engines like PostgreSQL often rewrite the correlated version into the same plan as a join, making them equivalent in practice.

SUMMARY

Summary — Correlated Subqueries

Summary

A correlated subquery is an inner query that references columns from the outer query, creating a data dependency that causes it to be conceptually re-evaluated for each outer row. The defining feature is the outer reference—a column from the outer query's table alias used inside the inner query's WHERE clause or other clauses. Correlated subqueries can appear in the WHERE clause (for row-level filtering), the SELECT list (as scalar computed columns), or with EXISTS / NOT EXISTS for efficient existence checks.

Unlike non-correlated subqueries that execute once and produce a static result, correlated subqueries are context-aware—they see different data for each outer row. While this makes them powerful for row-contextual logic, they can be performance-sensitive on large datasets without proper indexing or optimizer decorrelation. Always alias tables distinctly (especially for self-correlated queries), and consider whether a window function or LATERAL join might express the same logic more cleanly. Mastering correlated subqueries provides the mental model for understanding all row-dependent SQL constructs.

Varsity Tutors • SQL • Correlated Subqueries — Use correlated subqueries (intro)