Tier 4 · Advanced

Recursive CTEs in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

What are Recursive CTEs in SQL?

A recursive CTE lets a query reference itself, accumulating rows iteratively until nothing new turns up. It's designed for hierarchical data — org charts, category trees, folder structures — where the depth isn't known in advance and can't be handled with a fixed number of joins.

How do you write a recursive CTE in PostgreSQL?

The structure has two parts separated by UNION ALL: an anchor member and a recursive member. The anchor produces the starting rows. The recursive member references the CTE by name and extends the result by one step per iteration. PostgreSQL runs the anchor first, then keeps running the recursive member using the rows from the previous pass, until no new rows are produced.

The mechanics are easiest to see with a simple counter first:

WITH RECURSIVE numbers AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM numbers WHERE n < 10
)
SELECT n FROM numbers

The same structure traverses the org chart — the anchor is the root employee, each recursive step adds one level:

WITH RECURSIVE org_hierarchy AS (
    -- Anchor: start from the top (no manager)
    SELECT employee_id, name, manager_id, 1 AS depth
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Recursive: add each employee whose manager is already in the result
    SELECT e.employee_id, e.name, e.manager_id, oh.depth + 1
    FROM employees e
    JOIN org_hierarchy oh ON e.manager_id = oh.employee_id
)
SELECT employee_id, name, depth
FROM org_hierarchy
ORDER BY depth, name

The anchor selects the root: the employee with no manager. The recursive member joins each subsequent employee to whoever is already in the accumulated result, matching their manager_id to an employee_id already found. Each pass adds one level. Execution stops when no employees remain that can be joined to the current set.

The depth column is a counter that increments by one per level. Carrying a counter through the recursion is standard — useful for output ordering and for enforcing a termination limit.

What happens when a recursive CTE hits a cycle?

The one thing that trips people up

A recursive CTE terminates naturally when the recursive member returns zero new rows. That works correctly for clean tree structures. But if the data has cycles — a manager who reports to someone who reports back to them — the recursion never terminates and the query runs until it hits a resource limit.

PostgreSQL doesn't detect cycles automatically. The practical defense is a depth limit in the recursive member's WHERE clause:

WHERE oh.depth < 20

This stops recursion at 20 levels regardless of whether the data has further rows. It's a blunt guard, but it's reliable when you know a reasonable maximum depth.

Also: the RECURSIVE keyword in WITH RECURSIVE is required even when the recursion seems obvious. PostgreSQL uses it as a signal to enable the iterative evaluation mechanism. Leave it out and the query errors.

Should you use generate_series or a recursive CTE?

generate_series vs recursive CTEs

Use generate_series() when the sequence structure is known before the query runs — every day from A to B, every integer from 1 to N. Use a recursive CTE when the structure is encoded in the data and must be discovered during execution — traverse this org chart to whatever depth the data has.

Practice Recursive CTEs in SQL

Practice · easy ecommerce · Brightlane

Brightlane's data pipeline assigns sequential batch identifiers to processing jobs. Each identifier is one greater than the previous, starting at 1.

Write a query to return a sequence of batch identifiers from 1 through 5, with each value appearing as a separate row.

Output:

  • Five rows, with one column, job_id, containing the integers 1 through 5 in ascending order.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution
In the game

Station Zero, our free browser SQL game, teaches this concept inside a story. No signup.

trace the reactor with a recursive CTE in Station Zero

10 Recursive CTEs practice problems

Start learning to practice all 10 Recursive CTEs problems, with instant grading and mastery tracking.

Common questions about Recursive CTEs

Does a recursive CTE have to use UNION ALL?

No, UNION works too and deduplicates as it goes, which can stop a cyclic graph from looping forever. UNION ALL is the usual choice because it does less work, so reach for UNION deliberately when repeated rows are the problem you are solving.

What does the RECURSIVE keyword actually do?

It makes the CTE name visible inside its own definition. Without it the self-reference fails with the relation not existing, because a plain CTE is not in scope for itself. The keyword goes after WITH, once, even when several CTEs follow.

How do you stop a recursive CTE running away?

Carry a depth counter and test it in the recursive branch. It is a blunt guard, but PostgreSQL does not detect cycles for you, so a hierarchy with a loop in the data will otherwise run until it exhausts a resource limit.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.