Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 30 of 62 · 40 min
Recursive CTEs
Explanation
Why recursion?
Some questions have no fixed depth. "Everyone who reports to Meera" means her direct reports, their reports, their reports' reports, and so on. With self joins you would need one join per level and would have to know the number of levels in advance. A recursive CTE repeats one step until there is nothing left to find.
The two parts
- The anchor member: an ordinary SELECT that gives the starting rows, for example the number 1 or the employee at the top.
- The recursive member: a SELECT that reads the CTE itself and produces the next rows from the rows found in the previous round.
- They are joined with UNION ALL. The database runs the anchor once, then runs the recursive member again and again on the newest rows, until it returns no rows.
WITH RECURSIVE n(x) AS (
SELECT 1 -- anchor: start at 1
UNION ALL
SELECT x + 1 FROM n -- recursive member: next number
WHERE x < 10 -- stop condition
)
SELECT x FROM n;
-- 1, 2, 3, ... 10PostgreSQL, MySQL 8 and SQLite write WITH RECURSIVE. SQL Server and Oracle use the same idea but write just WITH.
A date series
WITH RECURSIVE d(day) AS (
SELECT '2025-03-01'
UNION ALL
SELECT date(day, '+1 day') FROM d WHERE day < '2025-03-05'
)
SELECT day FROM d;
-- 2025-03-01 ... 2025-03-05 (5 rows)This solves the "missing days" problem from module 5: generate every date (or month), then LEFT JOIN the sales onto it, so days with no sales show 0 instead of disappearing from a chart.
Walking the org chart
Start with the employee at the top (manager_id IS NULL) at level 1. In each round, find the employees whose manager is someone found in the previous round, at that level + 1.
WITH RECURSIVE chain(id, name, level) AS (
SELECT id, name, 1
FROM employees
WHERE manager_id IS NULL -- anchor: Meera
UNION ALL
SELECT e.id, e.name, c.level + 1
FROM employees AS e
JOIN chain AS c ON e.manager_id = c.id -- people managed by the previous round
)
SELECT name, level FROM chain ORDER BY level, name;
-- Meera Iyer | 1
-- Deepak Yadav | 2 ... Rahul Sharma | 2 (5 people)
-- Anita Desai | 3 ... Vikram Singh | 3 (5 people)To get everyone under one person, change the anchor: start from that person's direct reports. You can also walk upwards, from an employee to the top, by joining the other way: ON e.id = c.manager_id.
-- The chain of command above Neha Joshi
WITH RECURSIVE up(id, name, manager_id, step) AS (
SELECT id, name, manager_id, 0 FROM employees WHERE name = 'Neha Joshi'
UNION ALL
SELECT e.id, e.name, e.manager_id, u.step + 1
FROM employees AS e JOIN up AS u ON e.id = u.manager_id
)
SELECT step, name FROM up ORDER BY step;
-- 0 Neha Joshi, 1 Farhan Ali, 2 Meera IyerRecursion limits and cycles
If the data has a cycle (A manages B and B manages A, by mistake) or the stop condition is wrong, the recursion never ends. Engines protect you differently: SQL Server stops at 100 levels by default (change it with OPTION (MAXRECURSION n)); MySQL stops at cte_max_recursion_depth, 1000 by default; PostgreSQL and SQLite have no such default limit, so the query runs until you cancel it. PostgreSQL 14+ has a CYCLE clause to detect loops. Using UNION instead of UNION ALL removes repeated rows, which can stop some loops, but costs time.
Common mistakes
- No stop condition, so the query runs forever. Always put a WHERE in the recursive member (x < 10, day < end date), or rely on the join running out of rows.
- Forgetting RECURSIVE in PostgreSQL or MySQL, which gives an error that the CTE does not exist.
- A different number of columns in the anchor and the recursive member. Both SELECTs must return the same columns, in the same order.
- Joining the wrong way round in a hierarchy (e.id = c.manager_id walks up, e.manager_id = c.id walks down). Decide the direction first.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Recursive CTEs · 1/7
Some questions have no fixed depth, like everyone under a manager. A recursive CTE repeats one step until nothing new is found.
Prefer a video by a teacher? Find free videos on “sql recursive cte hierarchy org chart generate series tutorial” on YouTube ↗
In real life: Category trees and approval chains
Shopping sites store categories as a tree: Electronics → Mobiles → Smartphones, each row pointing to its parent. The breadcrumb at the top of a product page and the "all products under Electronics" listing both need a recursive walk of that tree. HR systems walk the reporting chain upwards to route a leave or expense approval, and bill-of-materials systems in manufacturing walk parts within parts.
WITH RECURSIVE chain(id, name, path) AS (
SELECT id, name, name FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, c.path || ' > ' || e.name
FROM employees AS e JOIN chain AS c ON e.manager_id = c.id
)
SELECT path FROM chain ORDER BY path;Practice (0/4 done)
Use a recursive CTE to return the numbers 1 to 10, one per row, in one column named x, in order.
Show the whole COMPANY reporting chain. Start from the employee with no manager at level 1; each person is one level below their manager. Return name and level, sorted by level and then by name.
Find everyone who reports to Rahul Sharma, directly or indirectly (not Rahul himself). Use a recursive CTE so it would still work if his team grew deeper. Return one column: name.
Make a month calendar and count orders in it. Generate the first day of each month from '2025-01-01' to '2025-06-01' with a recursive CTE (date(m, '+1 month') adds a month), LEFT JOIN orders by month, and return month (the 'YYYY-MM' text, substr(m, 1, 7)) and orders (the number of orders that month, 0 if none), in month order.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- UNION, INTERSECT and EXCEPTSubqueries, CTEs and set operations · same module
- Correlated subqueries and derived tablesSubqueries, CTEs and set operations · same module
- Recursion: functions that call themselvesData Structures and Algorithms (DSA) for interviews · Recursion and backtracking
- Subqueries, CTEs and window functionsBackend Development · SQL in depth
- Higher-order functions and recursionJavaScript Programming (language in depth) · Functions and closures
- RecursionC Programming (from zero to systems) · Functions and arrays
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What are the two parts of a recursive CTE?
Answer: An anchor member and a recursive member joined with UNION ALL
The anchor gives the start; the recursive member makes the next rows.
When does a recursive CTE stop?
Answer: When the recursive member returns no new rows
A WHERE stop condition, or a join that finds no more matches, ends it.
Walking DOWN an org chart, which join goes in the recursive member?
Answer: JOIN chain c ON e.manager_id = c.id
Find employees whose manager was found in the previous round. The other way walks up.
In SQL Server, a recursive CTE goes deeper than 100 levels. What happens by default?
Answer: It stops with an error; OPTION (MAXRECURSION n) changes the limit
SQL Server's default MAXRECURSION is 100.
Why generate a date series with a recursive CTE before joining sales?
Answer: So days with no sales still appear (with 0) after a LEFT JOIN
A table of sales has no rows for empty days; the calendar supplies them.
In PostgreSQL 12 and later, how is a CTE that is referenced only once usually run?Related
Answer: Inlined into the main query, unless you write AS MATERIALIZED
PostgreSQL 12 changed the default; earlier versions always materialised.
What is the extra space of a recursive function that goes n levels deep and creates no lists?Related
Answer: O(n)
n frames wait on the call stack at the deepest point, so the space is O(n).
What is a CTE?Related
Answer: A named temporary result written with WITH, used by the query after it
WITH name AS (...) defines a temporary named result that exists only for that query.
What does every recursive function need to stop?Related
Answer: A base case
The base case answers the smallest input directly without calling itself.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Subqueries, CTEs and set operations” before its exam: UNION, INTERSECT and EXCEPT.
- Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
- Then the next module, Window functions. 62 topics left in the course before the final exam.