How do you add a depth column to a recursive CTE that walks a manager_id chain?
answer
- the number has to start somewhere
- the anchor member sets the initial value
- the recursive member reads the previous row
- each iteration adds one to it
- put the ceiling where rows are generated
basics
~20 sInitialise the depth in the anchor member (0 or 1), then select depth + 1 in the recursive member. Each new row inherits its parent's depth plus one, so the column measures the distance in links from the starting node.
solid answer
~50 sThe depth is just another column you carry through the recursion. The anchor member gives it a constant — `0` if you want "edges from the start", `1` if you want a human-readable level number. The recursive member selects `parent.depth + 1`, reading the value from the **CTE reference**, not from the base table, so each iteration stamps rows one level deeper than the rows that produced them. If you want to stop at N levels, put the ceiling in the recursive member (`WHERE o.depth < 3`) so the engine never generates the deeper rows; a `WHERE depth <= 3` in the outer query walks the entire subtree first and throws most of it away. Depth is also the natural column for indentation and for `ORDER BY depth`, and it caps runaway walks — but it is not cycle detection.
code
sql · 13 linesWITH RECURSIVE org (id, name, manager_id, depth) AS (
SELECT e.id, e.name, e.manager_id, 0
FROM employees e
WHERE e.id = 42
UNION ALL
SELECT c.id, c.name, c.manager_id, o.depth + 1
FROM employees c
JOIN org o ON c.manager_id = o.id
WHERE o.depth < 3 -- ceiling where rows are generated
)
SELECT id, name, depth
FROM org
ORDER BY depth, name;go deeper
Be able to point at the two halves of a recursive CTE and say which one sets the starting depth and which one increments it. Knowing that the increment reads the CTE's own previous row is enough at this stage.
Explain the mechanics out loud: the recursive member joins the base table to the CTE, so depth + 1 comes from the CTE side. Show that you know where the level ceiling goes and why placement changes how much work the engine does.
Demonstrate judgment about cost and safety on real data — the ceiling inside the recursive member, the difference between a depth cap and true cycle handling, and the fact that the result has no guaranteed ordering without ORDER BY.
Own the convention: whether depth is 0- or 1-based, whether hierarchy depth is bounded by policy or only by hope, and when a repeatedly traversed adjacency list should be backed by a maintained derived structure instead of being walked on every request.
## What the depth column is for An adjacency list stores a hierarchy as one row per node with a self-referencing pointer: `employees(id, name, manager_id)`, `categories(id, name, parent_id)`. The table itself has no notion of "level" — a node is one link from its parent and that is all the schema knows. When you traverse the list with a recursive CTE, you almost always want to know *how far* each returned row sits from the row you started at: to indent an org chart, to answer "show me two levels down", to sort managers above their reports, or to detect that a tree has become unexpectedly deep. SQL gives you nothing automatic here. There is no hidden level pseudo-column in a `WITH RECURSIVE` query. You compute the depth yourself, as an ordinary expression, and carry it through the iterations. ## Where the number comes from A recursive CTE has two members joined by `UNION ALL`. The anchor member produces the starting rows; the recursive member joins the base table to *the CTE itself*, and re-runs until it produces nothing new. The depth column follows exactly that structure: ```sql WITH RECURSIVE org (id, name, manager_id, depth) AS ( SELECT e.id, e.name, e.manager_id, 0 FROM employees e WHERE e.id = 42 -- start node UNION ALL SELECT c.id, c.name, c.manager_id, o.depth + 1 FROM employees c JOIN org o ON c.manager_id = o.id -- o = the CTE, c = the table ) SELECT id, name, depth FROM org ORDER BY depth, name; ``` The key detail is which side `depth + 1` is read from. `o` is the reference to the CTE — the rows produced by the previous iteration — so `o.depth + 1` is "one deeper than the row that led me here". `c` is the base table and has no depth column at all. Getting these backwards is the usual beginner error and produces either a compile error or, if you alias sloppily, a nonsense column. Whether to start at `0` or `1` is a convention. `0` reads as "number of links from the anchor row", which makes the anchor row's own depth zero and matches how most people describe distance. `1` reads as "level 1 of the org chart". Either is fine; pick one and keep it consistent, because every filter you write afterwards (`depth <= 3`) shifts by one between the two. ## Capping the walk A depth ceiling belongs **inside the recursive member**: ```sql JOIN org o ON c.manager_id = o.id WHERE o.depth < 3 ``` That predicate is evaluated as the rows are generated, so the engine simply never produces level 4 and the recursion reaches its fixpoint immediately after level 3. Writing `WHERE depth <= 3` in the final `SELECT` instead is functionally different in cost: the CTE still expands the whole subtree — possibly the entire company — and the outer filter discards it. On a deep or wide tree that difference is enormous, and on a *cyclic* tree the outer-filter version never finishes at all. ## Depth is not cycle protection A depth cap bounds the work, which is why people reach for it when data may be dirty. But it is a blunt instrument: it silently truncates legitimately deep branches, and within the cap it still happily emits the same node repeatedly if the data loops. Real cycle handling means tracking the nodes already visited on the current path (or using the standard `CYCLE` clause where the engine implements it) and refusing to expand one twice. Use the depth cap for "I only want three levels", not for "my data might loop". ## Using depth in the output Depth drives presentation. Indentation is `depth` copies of a pad string concatenated in front of the name — engines spell the repeat function differently (`REPEAT` in PostgreSQL and MySQL, `REPLICATE` in SQL Server). Note that `ORDER BY depth` gives you a breadth-first listing: all level-1 rows, then all level-2 rows, with siblings from different parents interleaved. To print an indented tree where each node sits directly under its parent you need an accumulated path column to sort on; depth alone cannot express that ordering. Also remember that a recursive CTE, like any query, returns rows in no defined order unless you write `ORDER BY`. The engine happens to generate rows level by level, but relying on that without an explicit sort is a bug waiting for a plan change. ## Common mistakes Reading `depth` from the base table alias instead of the CTE alias; assuming the engine tracks recursion depth for you; putting the level ceiling in the outer query and calling it a termination condition; and mixing 0-based and 1-based conventions between the query and the application that consumes it.
- Why is a depth ceiling in the recursive member not a substitute for cycle detection?A cap bounds the work but does not understand the data. It truncates legitimately deep branches at the same limit, and within the cap it still re-emits the looping nodes over and over. Genuine cycle handling compares each candidate node against the set already visited on this path — a visited-path predicate, or the standard `CYCLE` clause — and refuses to expand a repeat.
- How would you render an indented org chart from the depth column?Concatenate `depth` copies of a pad string in front of the name — `REPEAT(' ', depth) || name` in PostgreSQL or MySQL, `REPLICATE(' ', depth) + name` in SQL Server. Depth alone will not order the rows correctly, though: `ORDER BY depth` interleaves siblings from different parents. You need an accumulated path column to sort on so each node prints directly beneath its own parent.
- Should depth start at 0 or 1, and does it matter?Both are used. Zero means "links traversed from the anchor row", so the starting node is depth 0; one means "level number" in the business sense. It only matters that the whole query and every consumer agree, because every `depth <= N` filter and every indentation calculation shifts by one between the conventions.
saying these in an interview costs you the question
- Says the engine exposes recursion depth in a hidden column
- Puts the depth ceiling in the outer WHERE and calls it a termination guard
- Reads depth + 1 from the base table alias, not the CTE alias
- Claims a depth cap makes cycle detection unnecessary
- Assumes rows come back ordered by depth without an ORDER BY