CMD Guide
HomeDatabasesSQL Fundamentals

Nested Query

A nested query (subquery) is just an inner SELECT whose result the planner must materialize or evaluate before the outer query can use it — and the single fact that decides everything about its cost is whether the inner query references a column from the outer query (correlated, re-run once per outer row) or not (uncorrelated, run once and reused).

That split is the whole topic. An uncorrelated subquery is a constant expression: the engine computes it a single time and substitutes the value (or value-set) into the outer predicate. A correlated subquery is a function of the current outer row — conceptually it executes again for every row the outer query examines, which is why a sloppy correlated subquery turns an O(N) scan into O(N×M). Real optimizers fight this by unnesting the subquery into a join when they legally can, so the same SQL can run as a nested loop or as a hash join depending on the planner. We will trace all three shapes against concrete rows.

The data we will trace

Two tables. We want the names of students who scored above 92 in at least one subject.

Students
student_idstudent_name
1Asha
2Ben
3Chen
4Dia
Exam_Results
student_idsubjectscore
1Math95
1Physics80
2Math88
3Chemistry93
3Math70

Note that Dia (id 4) has no exam rows at all — that absence is what breaks the NOT IN rewrite later.

Shape 1 — uncorrelated IN-subquery (runs once)

SELECT student_name
FROM   Students
WHERE  student_id IN (
    SELECT student_id
    FROM   Exam_Results
    WHERE  score > 92
);

The inner query mentions no column of Students, so it is evaluated once, producing a set. The outer query then tests each row's student_id for membership in that set.

  1. Inner runs first: scan Exam_Results, keep score > 92 → rows (1, Math, 95) and (3, Chemistry, 93). Project student_id → set {1, 3}. Computed one time.
  2. Outer scan, membership test against {1, 3}: Asha(1) ∈ → keep. Ben(2) ∉ → drop. Chen(3) ∈ → keep. Dia(4) ∉ → drop.
  3. Result: Asha, Chen.

Total inner executions: 1. This is the cheap shape.

diagram
diagram

Shape 2 — correlated subquery in SELECT (runs per row)

Same idea applied to a count-per-customer. Here the inner query references Customers.CustomerID — the outer row — so it is correlated:

SELECT c.Name,
       (SELECT COUNT(*)
        FROM   Orders o
        WHERE  o.CustomerID = c.CustomerID) AS OrderCount
FROM   Customers c;

Conceptually the engine walks each Customers row and runs the inner COUNT(*) once with that row's CustomerID bound in. With 10,000 customers that is 10,000 inner executions. The fix is almost always to rewrite it as an aggregate join, which the planner may also do for you:

SELECT c.Name, COALESCE(o.cnt, 0) AS OrderCount
FROM   Customers c
LEFT JOIN (
    SELECT CustomerID, COUNT(*) AS cnt
    FROM   Orders
    GROUP BY CustomerID
) o ON o.CustomerID = c.CustomerID;

Both return the same numbers, but the join scans Orders once and hashes it — O(N+M) instead of O(N×M). The correlated form is fine for a handful of rows and a poison pill at scale.

Shape 3 — scalar subquery in WHERE

A scalar subquery must return exactly one row, one column. "Salary above the company average":

SELECT Name, Salary
FROM   Employees
WHERE  Salary > (SELECT AVG(Salary) FROM Employees);

The inner AVG is uncorrelated, so it is computed once into a single number and the comparison reuses it for every outer row. If a scalar subquery ever returns two rows, the engine raises a runtime error (e.g. Postgres: "more than one row returned by a subquery used as an expression") — not a wrong answer, a hard failure.

The optimizer rewrites IN into a join

You wrote an IN-subquery, but on most engines the planner does not run a separate inner query and a membership test — it unnests the subquery into a semi-join. A semi-join returns each outer row at most once as soon as one match is found, which is exactly IN / EXISTS semantics. So your Shape-1 query is typically executed as:

-- logically what the planner runs (DISTINCT avoids row duplication)
SELECT s.student_name
FROM   Students s
JOIN   (SELECT DISTINCT student_id FROM Exam_Results WHERE score > 92) r
       ON r.student_id = s.student_id;

This is why "IN vs EXISTS vs JOIN" rarely matters for performance on a modern optimizer for the uncorrelated, NULL-free case — they collapse to the same plan. It starts to matter the moment NULLs or correlation enter, which is the next section. Read the plan with EXPLAIN rather than trusting the syntax you typed.

Pitfalls

Takeaways


Re-authored and deepened for this guide. Sources: PostgreSQL 16 documentation — "Subquery Expressions" and "Comparison Functions and Operators" (three-valued logic / NULL behavior of IN and NOT IN); the SQL:2016 standard's treatment of UNKNOWN and quantified comparison predicates; Markus Winand, SQL Performance Explained (semi-joins and subquery unnesting); and Joe Celko, SQL for Smarties (correlated subqueries and the NOT IN NULL trap). The original examples (IN-subquery, scalar > AVG, correlated OrderCount) were correct and are preserved; the execution mechanism, NULL semantics, and optimizer-rewrite material were added.

🎯 STRICT STANDOUT: Why / worked / when-not / failure / drills — Nested Query

Why this concept exists (judgment chain)

Subquery cost is dominated by correlation: uncorrelated runs once; correlated is conceptual N×M until the planner unnests to a semi-join/hash join. NOT IN with NULL is a semantic landmine; EXISTS/NOT EXISTS is the safe anti-semi-join form. Always verify with EXPLAIN.

Worked example with numbers or traced steps

Students 1..4; Exam scores: Asha Math95, Chen Chem93; Dia has no exams.
Uncorrelated IN score>92 → inner set {1,3} once → Asha,Chen.
Correlated COUNT orders per customer: 10k customers ⇒ ~10k inner plans unless rewritten to GROUP BY join.
NOT IN (SELECT student_id …) with a NULL student_id → entire outer result empty (UNKNOWN).
NOT EXISTS for Dia correctly returns Dia even if other NULLs exist.
Planner may rewrite IN to Hash Semi Join — syntax ≠ plan.

When NOT to use / named alternative

Keep a scalar uncorrelated AVG subquery when it is clear and the set is small. Prefer JOIN/EXISTS for large correlated patterns. Do not use IN when the subquery can return NULL keys. Scalar subqueries need cardinality 1 — use LIMIT 1+ORDER BY or aggregate.

Failure / ops fingerprint

Latency cliff when row count grows (hidden N+1). Mystery zero rows from NOT IN+NULL. Runtime error: more than one row returned by scalar subquery. Ops: EXPLAIN (ANALYZE) looking for Nested Loop with inner re-exec; track NOT IN in SQL linters.

Hostile-panel Q&As (model answers)

Q1. Correlated vs uncorrelated test?
Model answer: Does the inner SELECT reference an outer column? Yes → correlated.

Q2. Why NOT EXISTS over NOT IN?
Model answer: NOT IN becomes UNKNOWN if the set contains NULL; NOT EXISTS is true when no matching row exists, immune to that trap.

Q3. Is IN always slower than JOIN?
Model answer: Not on modern optimizers for uncorrelated NULL-free cases — they unnest to the same semi-join. Measure the plan.

🤖 Don't fully get this? Learn it with Claude

Stuck on Nested Query? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Nested Query** (Databases) and want to truly understand it. Explain Nested Query from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Nested Query** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Nested Query** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Nested Query** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes