skip to content

questions

12

What makes a subquery "correlated", and why does a correlated subquery risk being executed once per row of the enclosing query?

level: juniorimportance: must knowfreq 70%

answer

  1. references an outer column = correlated
  2. f(outer_row), re-evaluated per row
  3. uncorrelated = evaluate once, reuse
  4. flatten to join / semi-join / anti-join
  5. plan tell: loop count == outer rows

basics

~20 s

A correlated subquery references a column from the enclosing query, so its result depends on the current outer row and logically must be re-evaluated for each one. An uncorrelated subquery is self-contained: computed once, reused for every row.

solid answer

~50 s

A subquery is **correlated** when its body references a column supplied by the enclosing query block. That makes it a parameterized query: for outer row A it may return one answer, for outer row B another. The textbook execution model runs it once per outer row - a loop with a query inside the loop - so an N-row outer scan costs N executions of the inner query. An **uncorrelated** subquery names nothing from outside, so its result is constant for the statement. The engine evaluates it once, materializes or caches it, and reuses it. Optimizers do not accept the per-row model as final. In the rewrite phase they try to **decorrelate** (flatten) the correlated form into a join, semi-join or anti-join: the correlation reference becomes an ordinary join predicate, the inner table is scanned once, and hash or merge matching does the work in a single pass. Repeated per-row execution is the fallback when flattening is illegal.

code

sql · 8 lines
sql
-- uncorrelated: inner result is constant
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE country = 'DE');

-- correlated: references o.customer_id from the outer block
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM customers c
              WHERE c.id = o.customer_id AND c.country = 'DE');

go deeper

for a junior

Define correlation precisely (references an outer column) and state the per-row consequence with a rough cost multiplier.

for a middle

Add that the optimizer normally flattens it into a join or semi-join, and name the plan evidence you would look for.

for a senior

Frame it as a rewrite-phase decision: semantics are nested loops, execution is a join when flattening is legal; discuss what blocks it and the resulting cost multiplier.

for a principal

Discuss it as a portability and predictability issue - which engines flatten which shapes, and when you would standardize on explicit joins so plans stay stable across engines and versions.

## The two shapes A subquery is a query nested inside another. It sits in a predicate (`IN`, `EXISTS`, `= (...)`), in the projection list (a *scalar* subquery returning one row, one column), or in the `FROM` clause (a derived table). What drives optimizer behaviour is whether the subquery is self-contained. - **Uncorrelated**: every column it names resolves inside itself. Its result is a constant relation for the whole statement. - **Correlated**: at least one column resolves to the *outer* query block. The subquery is therefore a function of the outer row - conceptually `f(outer_row)`. ## Why correlation implies repetition SQL's semantics are defined over a nested-loop evaluation: for each candidate outer row, evaluate the predicate, which means evaluating the subquery with that row's values substituted. If the outer table has one million rows and the subquery costs one index probe, that is one million probes. If the subquery costs a table scan, it is one million scans - the difference between milliseconds and hours. Uncorrelated subqueries have no such multiplier: their answer cannot change between rows, so a single evaluation suffices and the engine can even build a hash table from it once. ## Decorrelation: turning the loop into a join The semantics are nested loops; the *execution* need not be. In the rewrite/normalization phase an optimizer tries to **flatten** the correlated subquery into a join-shaped operator over the same two relations, with the correlation column promoted to a join predicate. Once the query is a join, the optimizer's whole cost-based machinery applies: it can choose hash join, merge join or index nested loop, reorder the inputs, and pick which side to build from. A hash-based plan touches each table once - O(N + M) instead of O(N x M). The common flattenings are: `EXISTS`/`IN` predicates become semi-joins, `NOT EXISTS`/`NOT IN` become anti-joins, and scalar aggregate subqueries become an outer join against a pre-grouped aggregate. ## What you see in a plan A plan that failed to decorrelate shows the inner query as a separate subplan attached to a filter, often labelled a subplan, filter subquery, or a nested-loop whose inner side re-executes; the tell is an operator whose actual loop/execution count equals the outer row count. A decorrelated plan shows a join operator (hash semi join, merge anti join, nested loop semi join) with each input scanned once. Reading loop counts in a plan is how you tell which one you got. ## Practical consequences 1. Correlated subqueries are not automatically slow. On a modern optimizer most of them flatten and perform identically to the hand-written join. Rewriting them by hand out of superstition adds noise. 2. But flattening is not guaranteed. Certain constructs block it (row-limiting clauses, non-equality or `OR`-ed correlations, volatile functions, some outer-join positions), and then you are back to per-row execution. 3. Correlated *and* uncorrelated subqueries can both be cached: a correlated subquery whose parameter repeats may be served from a per-execution memo, which softens but does not remove the cost. 4. The fix when flattening fails is usually to write the join, semi-join or aggregate-then-join form yourself, so the plan cannot fall back to a loop. ## Cost intuition Think of the multiplier explicitly: cost approximately equals outer_rows x inner_cost for the un-flattened form, versus outer_cost + inner_cost + join_cost for the flattened one. Deciding whether a correlated subquery matters is a matter of estimating that multiplier: 50 outer rows with an index probe inside is fine; 5 million outer rows with a scan inside is a production incident.

  • If both forms are logically equivalent, why do people still report that rewriting a correlated subquery as a join made a query fast?
    Usually because the optimizer failed to decorrelate the original, so the plan really was re-executing the inner query per outer row. Hand-writing the join removes the construct that blocked flattening. It can also happen when the rewrite changes estimates enough to pick a better join order. It is not a general rule that joins beat subqueries.
  • How do you tell from an execution plan whether decorrelation happened?
    Look for a join-shaped operator (semi join, anti join, hash join) with each input scanned once. If instead you see the inner query as an attached subplan or filter, or a nested-loop inner side whose execution/loop count equals the outer row count, the subquery is being re-executed per row.

Uncorrelated is looking up one address before you leave the house; correlated is phoning the office again at every doorstep. Decorrelation is asking for the whole address list once and matching it in one pass.

saying these in an interview costs you the question

  • Claiming every correlated subquery is executed once per row on a modern optimizer
  • Saying subqueries are always slower than joins as a blanket rule
  • Confusing correlated with 'nested' - depth of nesting is not correlation
  • Thinking a derived table in FROM can never be correlated (LATERAL/APPLY forms are)
  • Assuming the engine caches the correlated result across all rows regardless of the parameter

context

open as a page

In a query plan, what does it mean to "push a predicate down", and what does the optimizer gain by doing it? Include projection pushdown in your answer.

level: juniorimportance: must knowfreq 70%

basics

~20 s

Pushdown moves a filter as close to the data source as possible - below joins and into scans - so fewer rows travel up the plan. Projection pushdown does the same for columns: read and carry only the columns actually needed. Both cut I/O, memory and CPU.

open as a page

When an optimizer flattens a WHERE EXISTS or WHERE IN subquery, what join shape does it produce, and how does that shape differ from an ordinary inner join?

level: middleimportance: must knowfreq 68%

basics

~20 s

It produces a semi-join: for each outer row it checks whether at least one matching inner row exists, then emits the outer row once and stops probing. An inner join emits one output row per matching pair, so it can duplicate outer rows and it exposes inner columns.

open as a page

Under what condition may an optimizer legally convert a LEFT OUTER JOIN into an INNER JOIN, and why is that conversion worth making?

level: middleimportance: must knowfreq 52%

basics

~20 s

When a predicate applied after the join rejects NULLs on the null-supplying side, the NULL-extended rows cannot survive anyway, so the outer join is equivalent to an inner join. Converting is worth it because inner joins can be reordered freely and get more access-path and pushdown options.

open as a page

In a relational database, what is the difference between a logical plan and a physical plan for the same query, which component produces each, and why does the distinction exist at all?

level: middleimportance: must knowfreq 55%

basics

~20 s

A logical plan says what result is wanted - relational operators like scan, filter, join, aggregate - with no algorithms. A physical plan says how: chosen access methods, join algorithms and order, with costs. Binding and rewriting produce the logical plan; the cost-based optimizer produces the physical one.

open as a page

Walk through what a relational database does with a SQL statement from the moment the text arrives at the server until rows come back to the client. Name each stage and say what it produces.

level: middleimportance: must knowfreq 62%

basics

~20 s

Parser checks grammar and builds a syntax tree. Binder resolves names and types against the catalog, producing a logical plan. Optimizer rewrites it and picks algorithms using statistics, producing a physical plan. Executor runs that plan tree and returns rows.

open as a page

One SQL statement fails with a message about a misspelled keyword; another parses fine but fails saying a referenced column does not exist. Explain why these two failures are detected at different points in query processing, and what the database knows at each point.

level: juniorimportance: should knowfreq 45%

basics

~20 s

The parser only checks the statement against the SQL grammar, so a misspelled keyword fails there. Names and types are checked later, during binding against the system catalog, so an unknown column fails only after the statement parses successfully.

open as a page

Why can a query engine usually turn a NOT EXISTS subquery into a plain anti-join, while a NOT IN subquery over a nullable column often cannot be flattened the same way?

level: middleimportance: should knowfreq 50%

basics

~20 s

NOT EXISTS is a clean 'no matching row' test, which maps directly onto an anti-join. NOT IN uses three-valued comparison: a single NULL in the subquery makes the predicate UNKNOWN for every outer row, so the engine must either prove both columns are NOT NULL or emit a costlier null-aware anti-join.

open as a page

Explain constant folding, OR-to-IN normalization and transitive predicate derivation as rewrite-phase transformations, and why writing a filter as YEAR(created_at) = 2024 keeps the rewriter from helping you.

level: middleimportance: should knowfreq 45%

basics

~20 s

Constant folding evaluates constant expressions once at planning time. OR-to-IN normalizes repeated equalities on one column into a single list so it can drive an index. Transitive derivation copies a predicate across an equality join, e.g. a.k = b.k with a.k = 5 also gives b.k = 5. Wrapping the column in a function leaves nothing to fold or match, so no index range is derivable.

open as a page

Give the situations in which a query engine cannot flatten a correlated subquery into a join, and describe what the resulting plan does instead.

level: seniorimportance: should knowfreq 42%

basics

~20 s

Flattening fails when the subquery's per-row semantics cannot be preserved by a set-oriented join: row-limiting clauses inside it, correlation under OR or in a non-equality predicate, volatile or side-effecting functions, some placements under an outer join, and correlations that reach past an aggregation. The plan then re-evaluates the subquery once per outer row.

open as a page

How does an optimizer handle a query that selects from a view or derived table, and which view definitions stop it from merging that view into the outer query?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A view is expanded into its definition and, when possible, merged (inlined) into the outer query so predicates and joins can be reordered across the boundary. Constructs that make merging unsound - DISTINCT, GROUP BY or aggregates, window functions, row limits, set operations, volatile functions - turn the view into a fence that is materialized or evaluated separately first.

open as a page

After optimization, the executor is handed a plan tree. Describe what that tree contains and how a typical relational executor consumes it to produce rows, including what changes when an operator cannot stream its output.

level: seniorimportance: should knowfreq 34%

basics

~20 s

The tree is physical operators - scans at the leaves, joins, aggregates and sorts above. The classic executor is demand-driven: each operator exposes open/next/close and pulls rows from its children. Streaming operators pass rows through; blocking ones like sort or hash build must consume their whole input first.

open as a page