Recursive CTEs help you find and show data that is connected in a chain, like family trees or company departments.
Recursive CTE for hierarchical data in PostgreSQL
WITH RECURSIVE cte_name (column_list) AS ( -- Anchor member: select the starting rows SELECT columns FROM table WHERE condition UNION ALL -- Recursive member: select rows related to previous rows SELECT columns FROM table JOIN cte_name ON join_condition ) SELECT * FROM cte_name;
The anchor member is the starting point of the recursion.
The recursive member repeats, adding related rows until no more are found.
WITH RECURSIVE subordinates AS ( SELECT employee_id, manager_id, name FROM employees WHERE manager_id IS NULL -- start from top manager UNION ALL SELECT e.employee_id, e.manager_id, e.name FROM employees e JOIN subordinates s ON e.manager_id = s.employee_id ) SELECT * FROM subordinates;
WITH RECURSIVE folder_tree AS ( SELECT folder_id, parent_folder_id, folder_name FROM folders WHERE parent_folder_id IS NULL -- root folders UNION ALL SELECT f.folder_id, f.parent_folder_id, f.folder_name FROM folders f JOIN folder_tree ft ON f.parent_folder_id = ft.folder_id ) SELECT * FROM folder_tree;
This example creates a simple employee table with managers and uses a recursive CTE to find all employees under the top manager Alice.
CREATE TABLE employees ( employee_id SERIAL PRIMARY KEY, manager_id INT, name TEXT ); INSERT INTO employees (manager_id, name) VALUES (NULL, 'Alice'), (1, 'Bob'), (1, 'Carol'), (2, 'David'), (2, 'Eve'), (3, 'Frank'); WITH RECURSIVE subordinates AS ( SELECT employee_id, manager_id, name FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.employee_id, e.manager_id, e.name FROM employees e JOIN subordinates s ON e.manager_id = s.employee_id ) SELECT * FROM subordinates ORDER BY employee_id;
Recursive CTEs run until no new rows are added, so be careful to avoid infinite loops.
Time complexity depends on the depth and number of related rows; it can be costly for very deep or large hierarchies.
Use recursive CTEs when you need to explore hierarchical or tree-like data; for flat data, simple queries are better.
Recursive CTEs let you query hierarchical data step-by-step.
Start with a base case, then add related rows repeatedly.
Useful for trees like org charts, folders, or parts lists.
Practice
WITH RECURSIVE clause in PostgreSQL when working with hierarchical data?Solution
Step 1: Understand the purpose of recursive CTEs
Recursive CTEs allow querying hierarchical data by starting with a base case and repeatedly adding related rows.Step 2: Compare options with this purpose
Only To repeatedly query related rows and build a hierarchy step-by-step describes this step-by-step building of hierarchy, while others describe unrelated SQL features.Final Answer:
To repeatedly query related rows and build a hierarchy step-by-step -> Option AQuick Check:
Recursive CTE = step-by-step hierarchy building [OK]
- Confusing recursive CTE with temporary tables
- Thinking recursive CTEs update data
- Assuming recursive CTEs join unrelated tables
tree in PostgreSQL?Solution
Step 1: Recall correct recursive CTE syntax
The correct syntax starts withWITH RECURSIVE cte_name AS ( ... ).Step 2: Match options to syntax
Only WITH RECURSIVE tree AS (SELECT ... UNION ALL SELECT ...) matches this exact syntax; others misplace the keyword RECURSIVE.Final Answer:
WITH RECURSIVE tree AS (SELECT ... UNION ALL SELECT ...) -> Option BQuick Check:
WITH RECURSIVE before CTE name [OK]
- Placing RECURSIVE after CTE name
- Using RECURSIVE inside parentheses
- Omitting RECURSIVE keyword
employees(id, name, manager_id) with data:1, 'Alice', NULL2, 'Bob', 13, 'Carol', 2What will the following query return?
WITH RECURSIVE emp_tree AS (SELECT id, name, manager_id FROM employees WHERE id = 1 UNION ALL SELECT e.id, e.name, e.manager_id FROM employees e JOIN emp_tree et ON e.manager_id = et.id) SELECT name FROM emp_tree ORDER BY id;
Solution
Step 1: Understand base case and recursion
The base case selects employee with id=1 (Alice). The recursive part adds employees whose manager_id matches ids in emp_tree.Step 2: Trace recursion
First iteration adds Bob (manager_id=1), second adds Carol (manager_id=2). So all three are included.Final Answer:
Alice, Bob, Carol -> Option DQuick Check:
Recursive CTE collects all subordinates [OK]
- Ignoring base case rows
- Confusing manager_id direction
- Assuming only direct reports are included
WITH RECURSIVE cte AS (SELECT id, parent_id FROM items WHERE parent_id IS NULL UNION ALL SELECT i.id, i.parent_id FROM items i JOIN cte ON i.parent_id = cte.id) SELECT * FROM cte;
It returns no rows. What is the most likely cause?
Solution
Step 1: Check base case condition
The base case selects rows where parent_id IS NULL. If none exist, base returns zero rows.Step 2: Understand recursion depends on base
Without base rows, recursion never starts, so no rows returned.Final Answer:
The base case SELECT returns no rows because no parent_id is NULL -> Option AQuick Check:
Base case must return rows to start recursion [OK]
- Assuming JOIN condition is wrong without checking data
- Thinking UNION vs UNION ALL causes no rows
- Believing ORDER BY affects recursion output
folders(id, name, parent_id) representing nested folders. Write a recursive CTE query to list all folders with their full path from the root, separated by '/'. Which query correctly achieves this?Solution
Step 1: Identify correct base case and path initialization
The base case must select root folders (parent_id IS NULL) and set path to just the folder name without leading '/'. The recursive part builds path as parent_path || '/' || name.Step 2: Compare options with correct logic
The correct query uses parent_id IS NULL for base, name AS path, and fp.path || '/' || f.name for recursion (PostgreSQL string concat). Distractors fail by using invalid + concat, adding extra '/' (causing '//'), or starting from non-roots (parent_id IS NOT NULL).Final Answer:
WITH RECURSIVE folder_paths AS (SELECT id, name, parent_id, name AS path FROM folders WHERE parent_id IS NULL UNION ALL SELECT f.id, f.name, f.parent_id, fp.path || '/' || f.name FROM folders f JOIN folder_paths fp ON f.parent_id = fp.id) SELECT id, path FROM folder_paths ORDER BY path; -> Option CQuick Check:
Use base root rows + '||' concat for paths [OK]
- Using '+' instead of '||' for string concatenation
- Starting recursion from non-root folders
- Adding extra '/' at path start
Ready to prove it?
Pass the test and this pattern moves unknown
