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.

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.

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.

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.

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.

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
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
SELECT 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
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.

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.

Structural and behavioral comparison between non-correlated and correlated subqueries
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
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.

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
Step 1 — Write the outer query skeletonStart 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
Step 2 — Write the correlated subqueryThe 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
Step 3 — Combine with comparison operatorPlace 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
Step 4 — Trace execution for one rowSuppose 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
Step 5 — Verify correctnessTo 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.

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.

Strengths and limitations of correlated subqueries
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
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.

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.

Mapping correlated subquery patterns to modern SQL alternatives
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.

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.

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.

PROBLEM 1CONCEPTUAL
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?
PROBLEM 2BASIC
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.
PROBLEM 3INTERMEDIATE
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.
PROBLEM 4APPLIED
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.
PROBLEM 5CRITICAL 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?

Summary — Correlated Subqueries

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)