Skip to content

Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 30 of 62 · 40 min

Recursive CTEs

Explanation

A recursive CTE refers to itself. It has an anchor member that gives the starting rows, and a recursive member that makes new rows from the previous ones, joined with UNION ALL, until a stop condition gives no more rows. Use it to generate number and date series and to walk hierarchies such as an org chart.

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

  1. The anchor member: an ordinary SELECT that gives the starting rows, for example the number 1 or the employee at the top.
  2. The recursive member: a SELECT that reads the CTE itself and produces the next rows from the rows found in the previous round.
  3. 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.
sql
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, ... 10
n(x) names the CTE and its one column. Without WHERE x < 10 it would never stop.

PostgreSQL, MySQL 8 and SQLite write WITH RECURSIVE. SQL Server and Oracle use the same idea but write just WITH.

A date series

sql
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)
SQLite's date(day, '+1 day') adds one day. PostgreSQL also has generate_series() for this.

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.

sql
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)
Round 1 finds Meera, round 2 her 5 direct reports, round 3 their 5 reports, round 4 finds nobody, so it stops.

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.

sql
-- 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 Iyer

Recursion 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

Recursive CTEs

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.

sql
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;
Builds breadcrumb-style paths, for example "Meera Iyer > Pooja Gupta > Kavya Menon".

Practice (0/4 done)

Task 1 · Code · SQL

Use a recursive CTE to return the numbers 1 to 10, one per row, in one column named x, in order.

Task 2 · Code · SQL

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.

Task 3 · Code · SQL

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.

Task 4 · Code · SQL

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

Question 1

1.What are the two parts of a recursive CTE?

Question 2

2.When does a recursive CTE stop?

Question 3

3.Walking DOWN an org chart, which join goes in the recursive member?

Question 4

4.In SQL Server, a recursive CTE goes deeper than 100 levels. What happens by default?

Question 5

5.Why generate a date series with a recursive CTE before joining sales?

Read more (free)

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.

From Common table expressions (WITH)

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).

From Recursion: functions that call themselves

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.

From Subqueries, CTEs and window functions

What does every recursive function need to stop?Related

Answer: A base case

The base case answers the smallest input directly without calling itself.

From Higher-order functions and recursion

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. 1 more topic in “Subqueries, CTEs and set operations” before its exam: UNION, INTERSECT and EXCEPT.
  2. Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Window functions. 62 topics left in the course before the final exam.

← Previous: Common table expressions (WITH)