Recursive CTEs
Query hierarchical and graph data without fixed-depth self-joins — org charts, category trees, bill-of-materials, path finding, and number generation using WITH RECURSIVE
Quick answer
A recursive CTE refers to itself: an anchor query produces starting rows, and a recursive query joined with UNION ALL keeps adding rows until it produces none. It walks hierarchies such as org charts and generates series.
- Anchor and recursive parts are joined with UNION ALL.
- Each step sees only the rows added by the previous step.
- Always include a stop condition, such as WHERE depth < 10.
WITH RECURSIVE chain AS (
SELECT employee_id, first_name, 1 AS level
FROM employees WHERE manager_id IS NULL AND store_id = 'ST001'
UNION ALL
SELECT e.employee_id, e.first_name, c.level + 1
FROM employees AS e
JOIN chain AS c ON e.manager_id = c.employee_id
)
SELECT * FROM chain ORDER BY level;Result · 4 rows
| employee_id | first_name | level |
|---|---|---|
| 1 | Sarah | 1 |
| 2 | Tyler | 2 |
| 3 | Amanda | 3 |
| 4 | Brandon | 3 |
// Part 01
The Problem — Hierarchies Have Unknown Depth
Most SQL queries work on flat data — each row is independent. But real-world data is often hierarchical: an employee reports to a manager who reports to a director who reports to a VP. A product category contains subcategories which contain sub-subcategories. A delivery route visits nodes that connect to other nodes.
Querying these structures with ordinary SQL requires knowing the depth in advance. To find all employees under a VP you would write a chain of self-joins — one join per level. This breaks the moment the hierarchy deepens or shallows. Recursive CTEs solve this cleanly: they repeat a query against their own previous output until no new rows are produced, traversing hierarchies of any depth without knowing the depth in advance.
// Part 02
Anatomy of a Recursive CTE
A recursive CTE has exactly two parts separated by UNION ALL: the anchor member (a regular SELECT that starts the recursion) and the recursive member (a SELECT that references the CTE itself, joining the previous iteration's output to the base table). The database alternates between running the recursive member and accumulating results until the recursive member produces zero new rows.
// Part 03
Number and Date Series Generation
The simplest recursive CTE generates a series of numbers or dates — anchoring at a start value and incrementing each iteration. This is a clean alternative to generate_series() and works across all SQL databases.
Integer series
Date series — fill calendar gaps
// Part 04
Hierarchical Data — Org Charts and Category Trees
The classic recursive CTE use case: traversing a parent-child relationship stored in a self-referencing table. The same table has both the child rows and the parent rows — the recursive CTE walks from root to leaves (top-down) or from leaf to root (bottom-up).
FreshCart employee org chart — top-down traversal
Bottom-up traversal — find all ancestors of an employee
Count reports at each level
// Part 05
Product Category Trees — Multi-Level Navigation
E-commerce category hierarchies are a textbook recursive CTE use case. Electronics → Mobiles → Smartphones → Android Phones. The same self-referencing table stores every level. The recursive CTE walks the tree to produce breadcrumb paths, flattened lists, or roll-up totals.
// Part 06
Path Finding — Shortest Route Between Nodes
Recursive CTEs can traverse graph structures — tables where rows represent edges (connections between nodes). The classic application is finding paths between stores, delivery waypoints, or network nodes. The recursive member builds paths by adding one edge per iteration.
// Part 07
Bill of Materials — Exploding Nested Components
A bill of materials (BOM) is a hierarchical parts list: a product contains components, each component contains sub-components, and so on. Recursive CTEs explode BOMs to compute total quantities and costs at every level — without knowing how many levels deep the nesting goes.
// Part 08
Cycle Detection and Prevention
A cycle occurs when following parent-child links eventually leads back to a node already visited — A is parent of B, B is parent of C, C is parent of A. Without protection, the recursive CTE loops infinitely. Two mechanisms prevent this: a depth limit (simple but imprecise) and a visited-array check (precise cycle detection).
// Part 09
Aggregating Over Hierarchies — Rollup Totals
Once a recursive CTE has mapped the full hierarchy, joining it back to fact data enables rollup aggregations — total sales for a category including all its subcategories, total headcount under each manager including all indirect reports.
// Part 10
What This Looks Like at Work
You are a data engineer at Amazon. The supply chain team needs a complete product component cost report — for any product, show every raw component it contains at any depth, the cumulative quantity needed, and the total cost contribution. The product hierarchy is 3–5 levels deep and changes weekly. They need this as an on-demand SQL query, not a hardcoded report.
🎯 Pro Tip
When building recursive CTEs for BOM explosion, always track cumulative_qty by multiplying the child's qty_per_parent by the parent's cumulative_qty at each level — not just the qty_at_level. A 2-unit parent containing 3-unit children means 6 children total, not 3. The multiplication must compound through every level of the hierarchy. Test the cumulative_qty column first on a small known BOM before running on production data.
// Part 11
Interview Prep — 5 Questions With Complete Answers
A recursive CTE is a Common Table Expression that references itself in its own definition. It has two parts separated by UNION ALL: an anchor member (a regular SELECT that runs once and seeds the recursion) and a recursive member (a SELECT that references the CTE by name and joins the previous iteration's output to the base table). The database alternates between running the recursive member against the previous iteration's rows and accumulating results, until the recursive member produces zero new rows.
Use a recursive CTE when the data has a hierarchical or graph structure stored in a self-referencing table — a parent_id column that points back to the same table's primary key. The key advantage over self-joins: recursive CTEs traverse hierarchies of unknown depth. A fixed chain of self-joins works only when you know the exact number of levels in advance. Recursive CTEs adapt automatically to any depth.
Common use cases: organisational hierarchies (employee → manager relationships), product category trees (category → parent category), bill of materials (product → components → sub-components), network/graph traversal (finding paths between nodes), and generating series (sequences of numbers or dates). Any time you find yourself writing a chain of self-joins to handle levels of a hierarchy, replace it with a recursive CTE — the query becomes shorter, more correct, and depth-independent.
The anchor member is the first SELECT in the recursive CTE — the one that does not reference the CTE itself. It runs exactly once when the CTE begins executing and produces the initial set of rows that seeds the recursion. For a top-down hierarchy traversal, the anchor typically selects root nodes (WHERE parent_id IS NULL). For a bottom-up traversal, the anchor selects a specific leaf node to start from. For a number series, the anchor selects the starting value (SELECT 1 AS n).
The recursive member is the second SELECT — it references the CTE by name and joins the CTE's rows (which represent the previous iteration's output) to the base table to find the next level of rows. Each iteration of the recursive member sees only the rows produced by the immediately preceding iteration — not all accumulated rows. The recursive member must produce fewer and fewer rows as the recursion progresses, eventually producing zero rows to terminate.
The two members are connected by UNION ALL (not UNION). Using UNION would deduplicate across iterations, which is both semantically wrong for most hierarchies and significantly slower. The final result of the CTE is the UNION ALL of all rows produced by all iterations — the anchor's rows plus every recursive iteration's rows combined. The outer SELECT that queries the CTE reads this complete accumulated result set.
Infinite recursion occurs when following parent-child links leads back to a node already visited (a cycle: A → B → C → A) or when the termination condition is never satisfied. Without protection, the recursive member keeps producing rows forever — and the behaviour differs by database. SQL Server has a built-in guard, OPTION (MAXRECURSION n), which defaults to 100 and raises "The maximum recursion 100 has been exhausted before statement completion" when hit (0 disables the limit entirely). PostgreSQL has no equivalent built-in cap — an unguarded recursive CTE with a cycle simply keeps running, consuming memory and disk, until you cancel the query or the server runs out of resources. On Postgres there is no default safety net to fall back on, so you must build your own termination guard.
Two protection mechanisms. Method 1: depth limit — add a depth counter to the anchor (0 AS depth) and increment it in the recursive member (depth + 1). Add WHERE depth < N to the recursive member. The recursion stops after N iterations regardless of data. Simple and cheap, but N must be chosen carefully — too low and valid deep hierarchies are cut off, too high and cycles spin for N iterations before stopping. For most org charts N = 10–20 is safe; for BOMs N = 5–8 is typical.
Method 2: visited array — maintain an array of node IDs seen in the current path. In the anchor, initialise it with ARRAY[id]. In the recursive member, append the new node's ID and add WHERE NOT (new_id = ANY(visited_array)). This stops the moment a cycle is detected, at exactly the right point regardless of depth. Cost: the array grows with depth and IS DISTINCT FROM comparisons are slightly more expensive than integer comparison. For production code on data that might have cycles, use both: the visited array for correctness and a depth limit as a safety backstop.
The pattern: anchor on the start date, recursive member adds one day per iteration, terminate when the date reaches the end date. WITH RECURSIVE date_series AS (SELECT '2024-01-01'::DATE AS dt UNION ALL SELECT dt + 1 FROM date_series WHERE dt < '2024-01-31') SELECT dt FROM date_series.
The primary use of date series in analytics is filling gaps in time-series data. When querying daily revenue, days with no orders produce no rows — the result has gaps. A date series CTE produces every calendar date, and a LEFT JOIN to the revenue data fills in zero for missing days. This ensures reports show a complete timeline without gaps, which is essential for charts and moving averages that depend on consecutive date sequences.
For monthly series, add INTERVAL '1 month' instead of +1: SELECT '2024-01-01'::DATE AS m UNION ALL SELECT (m + INTERVAL '1 month')::DATE FROM months WHERE m < '2024-12-01'. PostgreSQL also provides the built-in generate_series(start, end, interval) function which is more concise for this purpose, but the recursive CTE pattern is portable across all SQL databases that support WITH RECURSIVE (PostgreSQL, SQL Server, MySQL 8+, SQLite 3.35+).
A self-join joins a table to itself on a specific relationship — typically parent_id = id. A single self-join produces one level of the hierarchy: employees joined to their direct managers. Two self-joins produce two levels. N self-joins produce N levels. The critical limitation: the number of self-joins must be known and hardcoded. If the hierarchy deepens from 4 to 5 levels, the query must be rewritten to add another self-join. This is brittle — any schema change to the depth of the hierarchy requires a query change.
A recursive CTE traverses any depth automatically. The recursive member defines the relationship between one level and the next. The database applies this definition repeatedly until no new rows are produced. A 4-level hierarchy and a 40-level hierarchy use the identical query — only the data depth differs, not the SQL. This makes recursive CTEs the correct tool for any hierarchy where the depth is variable, unknown at query time, or changes over time.
Performance comparison: for shallow, fixed-depth hierarchies (always exactly 2-3 levels), self-joins can be marginally faster because the query plan is simpler and the planner can optimise fully. For variable-depth hierarchies, recursive CTEs are both more correct (they handle any depth) and practically faster (a 5-level self-join requires 5 table scans with Cartesian intermediate results; a recursive CTE performs one scan per level with only the relevant rows). The readability advantage strongly favours recursive CTEs — 10 lines replacing 50 lines of chained self-joins, with no depth limitation.
// Part 12
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓A recursive CTE has two parts joined by UNION ALL: an anchor member (runs once, seeds the recursion) and a recursive member (references the CTE itself, runs repeatedly until it produces zero rows).
- ✓The recursive member sees only the rows from the immediately preceding iteration — not all accumulated rows. Each iteration adds a new level of the hierarchy to the accumulated result.
- ✓Always include a termination condition: a depth counter with WHERE depth < N (simple) or a visited array with WHERE NOT (id = ANY(visited)) (precise cycle detection). Both together is safest for production.
- ✓Top-down traversal: anchor on roots (WHERE parent_id IS NULL), recursive member joins children to parents. Bottom-up traversal: anchor on a specific leaf, recursive member joins parent to child via manager_id = previous.employee_id.
- ✓Date series: anchor on start date, recursive increments by +1 or INTERVAL '1 month'. LEFT JOIN to fact data fills gaps with zero — essential for complete time-series charts and moving averages.
- ✓Build path strings by concatenating in the recursive member: path || ' → ' || new_name. Build sort keys with LPAD(id, 6, '0') so siblings sort correctly by numeric ID while maintaining lexicographic path order.
- ✓BOM explosion: cumulative quantity must multiply through every level — child.cumulative_qty = parent.cumulative_qty × child.qty_per_parent. Not just qty_per_parent alone.
- ✓Cycle prevention with visited array: ARRAY[id] in anchor, visited || new_id in recursive member, WHERE NOT (new_id = ANY(visited)) to stop. Use UNION (not UNION ALL) for DAGs where a node can have multiple parents.
- ✓Recursive CTEs replace chains of self-joins for hierarchical data. Self-joins require knowing the depth in advance; recursive CTEs handle any depth with the same query.
- ✓SQL Server enforces OPTION (MAXRECURSION n), default 100, and raises a clean error the moment it is exceeded. PostgreSQL has no equivalent built-in cap — an unguarded recursive CTE with a cycle just runs unbounded until it exhausts memory/disk. Never rely on a default limit that may not exist — always add your own termination guard.
What comes next
In Lesson 57, you learn EXPLAIN and Query Optimisation — reading execution plans, identifying bottlenecks, rewriting slow queries, and the systematic approach to making any query faster.
Lesson 57 → EXPLAIN and Query OptimisationQuick check
What stops a recursive CTE?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
