Recursive CTEs
A recursive CTE refers to itself, repeating until there's nothing new to add. It's how SQL walks hierarchies of unknown depth — org charts, category trees, threaded comments — and generates series of numbers or dates.
Counting to five
WITH RECURSIVE numbers(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM numbers WHERE n < 5 ) SELECT n FROM numbers;
| n |
|---|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
A recursive CTE has two parts joined by UNION ALL:
- The anchor (
SELECT 1) produces the starting rows. - The recursive part reads the rows produced in the previous step and produces new ones. It repeats until it returns no rows — here, when
nreaches 5.
WHERE n < 5, the recursion never ends. Many databases cap recursion depth; others will run until you cancel the query.Generating a date series
WITH RECURSIVE days(day) AS ( SELECT '2026-02-01' UNION ALL SELECT date(day, '+1 day') FROM days WHERE day < '2026-02-07' ) SELECT d.day, COUNT(o.id) AS orders FROM days AS d LEFT JOIN orders AS o ON o.order_date = d.day GROUP BY d.day;
| day | orders |
|---|---|
| 2026-02-01 | 0 |
| 2026-02-02 | 0 |
| 2026-02-03 | 1 |
| 2026-02-04 | 0 |
| 2026-02-05 | 0 |
| 2026-02-06 | 0 |
| 2026-02-07 | 0 |
A calendar of every day, joined to orders, shows days with zero orders that would otherwise be missing. (PostgreSQL has the built-in generate_series for this.)
Walking down a hierarchy
WITH RECURSIVE team(id, name, level) AS ( SELECT id, first_name, 0 FROM employees WHERE id = 3 UNION ALL SELECT e.id, e.first_name, t.level + 1 FROM employees AS e JOIN team AS t ON e.manager_id = t.id ) SELECT name, level FROM team;
| name | level |
|---|---|
| Mei | 0 |
| Amara | 1 |
| Jonas | 1 |
| Tomás | 1 |
Start with Mei (the anchor), then repeatedly add employees whose manager is already in the result. This finds everyone under her, however many levels deep.
A full org chart
WITH RECURSIVE chart(id, name, level, path) AS ( SELECT id, first_name, 0, first_name FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.first_name, c.level + 1, c.path || ' > ' || e.first_name FROM employees AS e JOIN chart AS c ON e.manager_id = c.id ) SELECT substr('············', 1, level * 3) || name AS org_chart, path FROM chart ORDER BY path;
| org_chart | path |
|---|---|
| Olivia | Olivia |
| ···Daniel | Olivia > Daniel |
| ······Hannah | Olivia > Daniel > Hannah |
| ···Ethan | Olivia > Ethan |
| ······Priya | Olivia > Ethan > Priya |
| ···Mei | Olivia > Mei |
| ······Amara | Olivia > Mei > Amara |
| ······Jonas | Olivia > Mei > Jonas |
| ······Tomás | Olivia > Mei > Tomás |
| ···Rafael | Olivia > Rafael |
| ······Lucas | Olivia > Rafael > Lucas |
| ······Sofia | Olivia > Rafael > Sofia |
Carrying a path column along shows each person's chain of command, and sorting by it groups each team under its manager. Repeating a character level times draws the tree.
Walking up a hierarchy
WITH RECURSIVE chain(id, name, manager_id) AS ( SELECT id, first_name, manager_id FROM employees WHERE first_name = 'Priya' UNION ALL SELECT e.id, e.first_name, e.manager_id FROM employees AS e JOIN chain AS c ON e.id = c.manager_id ) SELECT name FROM chain;
| name |
|---|
| Priya |
| Ethan |
| Olivia |
Reversing the join condition climbs from an employee up to the top — useful for breadcrumbs in category trees.
Dialect notes
- PostgreSQL, MySQL 8+ and SQLite require the
RECURSIVEkeyword; SQL Server and Oracle don't use it (justWITH). - SQL Server limits recursion to 100 levels by default (
OPTION (MAXRECURSION n)changes it); MySQL's limit iscte_max_recursion_depth, 1000 by default. - Data with cycles (A manages B, B manages A) recurses forever unless you track visited rows; PostgreSQL 14+ has a
CYCLEclause for this.
Try it yourself
Generate the first ten powers of two.