CTEs and Subqueries
Name intermediate results; compose instead of nest
Common Table Expressions name intermediate results, making complex logic readable and composable — and recursive CTEs walk trees and hire charts.
What you will learn
- Structure multi-step SQL with WITH clauses
- Know when a CTE materializes vs inlines in the planner
- Use recursive CTEs for hierarchies
What is this?
A CTE (`WITH name AS (SELECT ...)`) defines a named result you can reference later in the query, readable top-down instead of nested inside parentheses. Subqueries are anonymous versions of the same idea inside FROM/WHERE. Recursive CTEs extend this to walk trees and graphs row by row.
Why do you need it?
- CTEs convert unreadable 40-line nested queries into a sequence of named, reviewable steps.
- Recursive CTEs replace application-level tree-walking logic when you must model hierarchies.
- Good CTE names document intent — the same way function names document code.
Mental model
Labeling jars while cooking
CTEs
You wouldn't keep every intermediate in an unlabeled bowl; you label them (‘mirepoix’, ‘roux’) to assemble the dish. CTEs label each intermediate result so the final SELECT reads like a recipe.
How it works
- WITH raw AS (...), clean AS (...) lets later CTEs reference earlier ones.
- A subquery in FROM behaves like a derived table you can't reference twice.
- Planners in modern warehouses often inline CTEs (a 'view'), not materialize; only mutually-recursive or referenced-multiple-times CTEs stage disk.
- Recursive CTEs have a base term and a recursive term joined by UNION ALL.
- The recursion terminates with an anchor and a WHERE that bounds the walk.
Layered CTE composition.
Examples
A readable multi-step pipeline in SQL
WITH cleaned AS ( SELECT id, COALESCE(amount, 0) AS amount FROM raw_events WHERE id IS NOT NULL),daily AS ( SELECT substr(ts, 1, 10) AS day, SUM(amount) AS revenue FROM cleaned GROUP BY 1)SELECT day, revenue, ROUND(revenue / 1000.0, 2) AS kFROM dailyORDER BY day;Each named step is independently reviewable; the final select is a composition of named results.
In production
- dbt models naturally taste like CTE chains — warehouse transforms are written this way and reviewed line by line.
- A dedup pipeline (ranked CTE → filter rn=1 → join dims) keeps each concern in its own named block.
- An org-hierarchy report walks the reporting tree with a recursive CTE to depth N.
Best practices
- Give CTEs intent-revealing names; a query of 8 anonymous subqueries is a maintenance hazard.
- Prefer CTEs over deep nesting for readability, and check the plan when a 'free' CTE is materializing twice.
- Use recursion only where the DB is the right tree-walker (small/medium trees); huge graphs belong in Spark/graph engines.
- Put the final, user-facing projection last — the first lines should be the rawest stage.
Common mistakes
- Stacking subqueries six deep until the query is unreadable and un-diffable.
- Assuming CTEs are always materialized — the planner may inline them repeatedly, killing performance.
- Forgetting UNION ALL (not UNION) in a recursive CTE, deduping every level and corrupting tree counts.
- Infinite recursion from a missing termination predicate.
Common issues
Recursive CTE never terminatesMissing depth guard or unbounded step
Add a WHERE on a depth column in the recursive term and confirm base + step decrease.
CTE-like logic duplicated in the planThe planner inlined a multi-referenced CTE instead of materializing
Force materialization (MATERIALIZED in PostgreSQL) or refactor to avoid repeated computation.
Debugging tips
- Run each CTE body alone and inspect its output before assembling the full query.
- If a CTE is slow, check EXPLAIN for whether it was inlined or materialized.
- For recursion issues, print the iteration depth column and bound it.
Interview checkpoint
Hands-on lab
- Chain named CTE stages readably
- Use a CTE for dedup + a CTE for aggregation
- Debug by running each CTE in isolation
Compose a Multi-Step Transform with CTEs
25 min · intermediate
Quick quiz
Which keyword introduces a named reusable intermediate result in modern SQL?
Planners always materialize CTEs to disk exactly once.