All 40 lessons

Lesson 24 of 40 · Subqueries & CTEs

SQL Recursive CTE

By Skillancy Editorial Team

Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.

A recursive CTE is a CTE that refers to itself, so it can repeat a step until there's nothing left to add. It's the standard way to walk hierarchies (an org chart, category trees, referral chains) and to generate sequences of numbers or dates.

How it works

WITH RECURSIVE cte_name AS (
  -- 1. Anchor: the starting rows
  SELECT ...
  UNION ALL
  -- 2. Recursive step: new rows built from the previous step
  SELECT ...
  FROM cte_name
  JOIN ... ON ...
)
SELECT * FROM cte_name;

The database runs the anchor once, then runs the recursive step on the rows it just produced, then again on those rows, and so on until a step returns nothing.

Examples

1. Count from 1 to 5

The simplest possible recursion:

WITH RECURSIVE numbers AS (
  SELECT 1 AS n          -- anchor
  UNION ALL
  SELECT n + 1           -- recursive step
  FROM numbers
  WHERE n < 5            -- stop condition
)
SELECT n FROM numbers;
Try it in the SQL Compiler

Result:

n
1
2
3
4
5

(5 rows)

2. The full org chart with levels

Start from the CEO (no manager), then repeatedly add each person's direct reports:

WITH RECURSIVE org AS (
  SELECT employee_id, employee_name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL                 -- anchor: the top

  UNION ALL

  SELECT e.employee_id, e.employee_name, e.manager_id, org.level + 1
  FROM employees e
  JOIN org ON e.manager_id = org.employee_id   -- next level down
)
SELECT level, employee_name
FROM org
ORDER BY level, employee_name;
Try it in the SQL Compiler

Result:

levelemployee_name
1Rajesh Khanna
2Amit Verma
2Neha Joshi
2Pooja Sinha
3Kavya Pillai
3Manish Tiwari
3Ritika Bose
3Sana Qureshi
3Suresh Rao
3Tanvi Shah
4Deepak Yadav
4Harish Gowda

(12 rows)

A self join only reaches one level; recursion reaches every level, however deep.

3. Reporting chain as a path

Build a readable path for each employee:

WITH RECURSIVE chain AS (
  SELECT employee_id, employee_name,
         employee_name::text AS path
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.employee_id, e.employee_name,
         chain.path || ' > ' || e.employee_name
  FROM employees e
  JOIN chain ON e.manager_id = chain.employee_id
)
SELECT employee_name, path
FROM chain
WHERE employee_name IN ('Harish Gowda', 'Deepak Yadav', 'Tanvi Shah');
Try it in the SQL Compiler

Result:

employee_namepath
Tanvi ShahRajesh Khanna > Pooja Sinha > Tanvi Shah
Deepak YadavRajesh Khanna > Amit Verma > Manish Tiwari > Deepak Yadav
Harish GowdaRajesh Khanna > Neha Joshi > Suresh Rao > Harish Gowda

(3 rows)

The ::text cast in the anchor lets the path column grow beyond the original column length.

4. Everyone under one manager

All of Neha Joshi's team, direct and indirect:

WITH RECURSIVE team AS (
  SELECT employee_id, employee_name
  FROM employees
  WHERE employee_name = 'Neha Joshi'

  UNION ALL

  SELECT e.employee_id, e.employee_name
  FROM employees e
  JOIN team t ON e.manager_id = t.employee_id
)
SELECT employee_name
FROM team
WHERE employee_name <> 'Neha Joshi';
Try it in the SQL Compiler

Result:

employee_name
Suresh Rao
Kavya Pillai
Harish Gowda

(3 rows)

5. Generate a date series

Every day in the first week of 2026, a useful base for daily reports with no gaps:

WITH RECURSIVE days AS (
  SELECT DATE '2026-01-01' AS day
  UNION ALL
  SELECT (day + 1)::date
  FROM days
  WHERE day < DATE '2026-01-07'
)
SELECT day FROM days;
Try it in the SQL Compiler

Result:

day
2026-01-01
2026-01-02
2026-01-03
2026-01-04
2026-01-05
2026-01-06
2026-01-07

(7 rows)

In PostgreSQL, generate_series does this more simply, but the recursive version works in MySQL 8 and SQLite too.

Common mistakes

  • No stop condition. SELECT n + 1 FROM numbers without WHERE n < 5 runs until the database stops it.
  • UNION vs UNION ALL. UNION ALL is standard and faster; UNION removes duplicates, which can hide data issues but also stops some cycles.
  • Type mismatch between anchor and recursive part, for example a VARCHAR(100) anchor and a longer concatenated string. Cast in the anchor.

Practice question

Count how many people (direct and indirect) report to each of Rajesh Khanna's direct reports.

Show solution
WITH RECURSIVE tree AS (
  SELECT employee_id, employee_id AS root_id
  FROM employees
  WHERE manager_id = 1               -- Rajesh's direct reports are the roots

  UNION ALL

  SELECT e.employee_id, t.root_id
  FROM employees e
  JOIN tree t ON e.manager_id = t.employee_id
)
SELECT m.employee_name AS leader,
       COUNT(*) - 1 AS people_below
FROM tree t
JOIN employees m ON m.employee_id = t.root_id
GROUP BY m.employee_name
ORDER BY people_below DESC;
Try it in the SQL Compiler

Result:

leaderpeople_below
Neha Joshi3
Amit Verma3
Pooja Sinha2

(3 rows)

COUNT(*) - 1 excludes the leader themselves.

Key takeaways

  • WITH RECURSIVE = anchor query + UNION ALL + recursive step.
  • The recursive step reads from the CTE itself and must eventually return no rows.
  • Use it for hierarchies of any depth and for generating sequences.
  • Track level or a path column to understand the structure.
Want every variation on one page?CTE in SQL: the complete guide

Frequently asked questions

What is a recursive CTE?

A CTE that refers to itself. It has an anchor query that produces the starting rows, then a recursive query that repeatedly adds rows based on the previous step until no new rows appear.

How do I stop a recursive CTE from running forever?

Make sure the recursive part eventually returns no rows, for example by adding WHERE n < 10 or by following a hierarchy that ends. Cycles in the data, such as A managing B and B managing A, need a depth limit or a visited-path check.

Do all databases support recursive CTEs?

PostgreSQL, MySQL 8+, SQLite and Snowflake use WITH RECURSIVE. SQL Server and Oracle support recursive CTEs with plain WITH.

Newsletter

Keep learning SQL

One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Prefer a one-page summary? Download the free SQL cheat sheet (PDF)