In a recursive CTE over (id, parent_id), what changes when you walk ancestors instead of descendants?
answer
- the table stores one edge per row
- only the recursive member's join predicate moves
- which side carries id and which carries parent_id
- one direction fans out, the other is a chain
- the anchor row is in both results
basics
~20 sThe join direction flips. For descendants you join the table's parent_id to the CTE's id; for ancestors you join the table's id to the CTE's parent_id. The anchor — the node you start from — is written the same way in both.
solid answer
~50 sAn adjacency list is a directed edge per row, and a recursive CTE can follow that edge either way. Both queries anchor on the same node; only the recursive member's join condition changes. Going **down** (descendants, the subtree): `JOIN tree t ON child.parent_id = t.id` — find rows whose parent is a row I already have. Going **up** (ancestors, the chain to the root): `JOIN tree t ON parent.id = t.parent_id` — find the row that is the parent of a row I already have. The shapes of the results differ too. Downward the walk fans out: a node can have many children, so each iteration can multiply rows. Upward it is a single chain, because a node has at most one parent, and it terminates naturally when `parent_id IS NULL` yields no match. Both results include the anchor row itself unless you filter it out.
code
sql · 9 lines-- descendants: which rows name a row I already have as parent?
WITH RECURSIVE subtree (id, name, parent_id) AS (
SELECT c.id, c.name, c.parent_id FROM categories c WHERE c.id = 7
UNION ALL
SELECT child.id, child.name, child.parent_id
FROM categories child
JOIN subtree s ON child.parent_id = s.id
)
SELECT * FROM subtree;go deeper
Be able to write both walks over a table with id and parent_id and say in plain words which join predicate finds children and which finds the parent. This is a common first-screen exercise for data roles.
Explain why the two directions behave differently at runtime: one fans out into a whole subtree, the other is a chain bounded by the tree's height. Mention that the anchor row is in the output and how you would drop it.
Show that you notice when a question secretly needs both walks — up to a subtree root, then down — and that following edges in both directions at once turns a tree into a graph that needs cycle protection.
Frame the read pattern: if ancestor lookups are on a hot path and the tree is deep, the choice is not which join to write but whether the hierarchy should be materialised so no walk is needed per request.
## Two directions over one adjacency list A self-referencing table — `categories(id, name, parent_id)`, `employees(id, name, manager_id)` — stores a hierarchy as one directed edge per row: *this row points at its parent*. The table is symmetric in the sense that the same edge answers two different questions: - **Descendants**: given a node, which rows sit beneath it (its subtree)? - **Ancestors**: given a node, which rows sit above it (the chain up to the root)? A recursive CTE can walk either direction over exactly the same table. Nothing about the schema changes; one join predicate does. ## Descendants: following the edge backwards, fanning out ```sql WITH RECURSIVE subtree (id, name, parent_id) AS ( SELECT c.id, c.name, c.parent_id FROM categories c WHERE c.id = 7 -- start node UNION ALL SELECT child.id, child.name, child.parent_id FROM categories child JOIN subtree s ON child.parent_id = s.id ) SELECT * FROM subtree; ``` Read the recursive member as a question: *which table rows name a row I already have as their parent?* That is `child.parent_id = s.id`. Because many rows can name the same parent, each iteration can produce several rows for every row of the previous iteration — the walk fans out, and the total result is the whole subtree. Its size is bounded by the tree, not by its height. ## Ancestors: following the edge forwards, a single chain ```sql WITH RECURSIVE ancestors (id, name, parent_id) AS ( SELECT c.id, c.name, c.parent_id FROM categories c WHERE c.id = 7 -- start node UNION ALL SELECT p.id, p.name, p.parent_id FROM categories p JOIN ancestors a ON p.id = a.parent_id ) SELECT * FROM ancestors; ``` Now read it as: *which table row is the parent of a row I already have?* That is `p.id = a.parent_id`. Since `parent_id` holds at most one value and `id` is unique, each iteration yields at most one row per row it consumed. The walk is a chain, and it stops on its own: when it reaches the root, `parent_id` is NULL, the join finds nothing, and the recursion hits its fixpoint. The number of iterations is the height of the tree, which is typically small. ## What stays the same The anchor member is identical in both queries — you always start by selecting the node of interest. The `UNION ALL` structure is identical. Any extra columns you carry (a depth counter, an accumulated path) work the same way; only their meaning changes, since depth going up counts steps toward the root rather than away from it. ## Including or excluding the starting row Both queries return the anchor row. That is usually what you want for a subtree ("this category and everything under it") but often not for ancestors ("who are this employee's managers" rarely means "including himself"). Exclude it in the outer query — `WHERE id <> 7` — or carry a depth column and filter `depth > 0`. Deciding this after the fact in the outer `SELECT` is fine; do not try to skip it by starting the anchor one level up, which breaks when the node is already the root. ## Ordering the results Ancestor queries usually want root-first output for a breadcrumb, but the walk generates them child-first. Carry a depth column and `ORDER BY depth DESC`, or accumulate a path. As always, the rows have no guaranteed order without an explicit `ORDER BY`. ## When it looks like both directions at once "Everyone in the same department subtree as this employee" is two walks, not a clever one: go up to find the subtree root, then go down from it. You can write it as two stacked CTEs, the second anchored on the result of the first. Trying to express it as one recursion that follows edges in both directions turns the tree into an undirected graph, which means every node reaches every other node and the query almost certainly needs cycle protection. ## Common mistakes Joining the base table to itself instead of to the CTE — the recursive member must reference the CTE name or it is not recursion at all. Assuming an upward walk can fan out, which only happens if a row has multiple parents (a graph, not a tree). Forgetting that the anchor row is in the output. And reversing `ORDER BY` in the hope that it changes which rows the query finds; ordering is presentation, the join predicate is what chooses the direction.
- Why does an ancestor walk terminate naturally while a descendant walk needs more care?Going up, each row has at most one parent, so an iteration yields at most one row and the chain ends at the root where `parent_id IS NULL` matches nothing — the number of iterations equals the tree's height. Going down, a row can have many children, so the result set is the whole subtree and can be very large. Neither is safe if the data contains a loop.
- How would you return only the ancestors, excluding the node you started from?Carry a depth column starting at 0 in the anchor and filter `WHERE depth > 0` in the outer query, or filter the id directly with `WHERE id <> :start_id`. Do not try to anchor one level up instead — that query returns nothing when the node is already the root, and it duplicates the parent lookup.
saying these in an interview costs you the question
- Thinks the recursive member joins the base table to itself
- Assumes an upward walk can fan out like a downward one
- Says you need a different table or an extra column to go up
- Forgets the starting row appears in the result
- Believes reversing ORDER BY changes the traversal direction