Recursive CTE explained — org charts, trees, and when you actually need one

Published Aug 2, 2026·9 min read·Every query runnable in your browser

A recursive CTE is a query with three moving parts: an anchor (where to start), a step (how to grow), and a stop (no new rows → done). That's it. If you can say those three sentences about your problem — "start at Bob, follow manager_id upward, stop at the CEO" — you can write the query. This post walks a real 50-person org chart in both directions.

In this post

  1. The shape: anchor + step + stop
  2. Walking UP: everyone above Bob
  3. Walking DOWN: everyone under Eva
  4. The other use: generating sequences
  5. The infinite-loop trap
  6. When NOT to use recursion
  7. FAQ

The shape: anchor + step + stop

The skeleton every recursive CTE follows
WITH RECURSIVE walk AS (
  -- 1. ANCHOR: the starting row(s). Runs once.
  SELECT ... FROM t WHERE <start condition>

  UNION ALL

  -- 2. STEP: joins the PREVIOUS round back to the table. Repeats.
  SELECT ... FROM t JOIN walk ON <how rows connect>
)
-- 3. STOP is implicit: a round that adds no rows ends the recursion.
SELECT * FROM walk;

The engine does not "call itself" like a programming-language function. It runs the anchor, then repeatedly runs the step against the rows the previous round produced, accumulating results, until a round comes back empty. It is a loop with a growing pile, not a call stack.

Walking UP: everyone above Bob

SQL Quest's employees table stores hierarchy the standard way: each row carries a manager_id pointing at another row. "Who is above Bob Smith?" has unknown depth — that is the recursion signal.

Manager chain, bottom to top — runs on the real table
WITH RECURSIVE chain AS (
  SELECT emp_id, name, manager_id, 1 AS level
  FROM employees WHERE name = 'Bob Smith'       -- anchor: start at Bob

  UNION ALL

  SELECT e.emp_id, e.name, e.manager_id, chain.level + 1
  FROM employees e
  JOIN chain ON e.emp_id = chain.manager_id      -- step: my manager's row
)
SELECT name, level FROM chain ORDER BY level;
namelevel
Bob Smith1
Alice Johnson2
Eva Martinez3

Three rounds: Bob (anchor) → Bob's manager_id points at Alice → Alice's points at Eva → Eva's manager_id is NULL, the join matches nothing, the round comes back empty, recursion stops. The stop condition is the data itself running out — nobody wrote a loop counter.

Walking DOWN: everyone under Eva

Same skeleton, join flipped: instead of "my manager's row," the step finds "rows whose manager is someone I already found."

Whole subtree under Eva Martinez — direction is just the ON clause
WITH RECURSIVE team AS (
  SELECT emp_id, name, 0 AS depth
  FROM employees WHERE name = 'Eva Martinez'

  UNION ALL

  SELECT e.emp_id, e.name, team.depth + 1
  FROM employees e
  JOIN team ON e.manager_id = team.emp_id       -- flipped: who reports to me?
)
SELECT name, depth FROM team ORDER BY depth, name;

On the real data this returns Eva at depth 0, her 10 direct reports at depth 1, and their reports below — the whole subtree, one query, no idea beforehand how deep it went. Compare the two ON clauses: e.emp_id = chain.manager_id walks up, e.manager_id = team.emp_id walks down. The direction of a hierarchy walk lives entirely in that one line.

Interview framing: "traverse the org chart" questions at Amazon, Stripe and the AI labs are exactly this skeleton. What they grade is whether you know the three parts — most candidates write the anchor, fumble the step's join direction, and never mention termination.

The other use: generating sequences

Recursion can also manufacture rows from nothing — numbers, dates, time buckets:

Numbers 1 through 5, no table required
WITH RECURSIVE n(x) AS (
  SELECT 1                          -- anchor
  UNION ALL
  SELECT x + 1 FROM n WHERE x < 5  -- step + EXPLICIT stop
)
SELECT x FROM n;   -- 1, 2, 3, 4, 5

Note the difference from the org chart: here the data never runs out on its own, so the stop lives in the step's WHERE x < 5. Swap 5 for a date and +1 for date(x, '+1 day') and you have a calendar table — the standard fix for "my report skips days with no sales."

The infinite-loop trap

The one way recursive CTEs hurt you: a cycle. If the data ever says A manages B and B manages A — one bad UPDATE is enough — the walk revisits rows forever, and the query runs until the engine's safety net (or your DBA) kills it.

Two defenses, use at least one on data you don't fully trust:

When NOT to use recursion

Rule of thumb: recursion is for unknown depth. Known depth = joins. Unknown depth = WITH RECURSIVE.

▶ Start with the plain WITH — recursion is one UNION ALL away

FAQ

Is a "nested CTE" the same as a recursive CTE?

No. "Nested" usually means several plain CTEs where later ones reference earlier ones — a pipeline, no self-reference, nothing recursive about it. A recursive CTE references itself inside its own definition. Pipelines: CTE tutorial. Self-reference: this post.

Which databases support WITH RECURSIVE?

All the majors. PostgreSQL, MySQL 8+, SQLite and MariaDB require the RECURSIVE keyword; SQL Server and Oracle use plain WITH and detect the self-reference. The skeleton is identical everywhere.

How deep can it go?

Engine-dependent: SQLite defaults to 1,000 iterations, SQL Server to 100 (OPTION (MAXRECURSION n) to change), PostgreSQL and MySQL are bounded by memory/config. If you are hitting these limits on an org chart, check for a cycle before raising them.

Anchor. Step. Stop. Now write one.

SQL Quest's CTE track runs from your first WITH to a Hard recursive org-chart traversal — in your browser, with a Coach that explains why a walk is wrong.

Practice CTEs — Free →