By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(For Data Analysts Who Need to Traverse Org Charts, Bill of Materials, or Any Nested Data in SQL)
You’re handed a table of employees where each row has an employee_id and a manager_id. Your boss asks: - "Show me the full reporting chain for Alice, all the way up to the CEO." - "How many direct and indirect reports does Bob have?" - "If we fire Carol, who loses their manager?"
employee_id
manager_id
Or you’re working with a Bill of Materials (BOM)—a table listing parts and their subcomponents (e.g., a car has an engine, which has pistons, which have rings). Your supply chain team needs: - "What’s the total cost of all subcomponents for Product X, 5 levels deep?" - "Which parts are used in multiple products?"
Without recursive CTEs, you’re stuck writing:- Nested self-joins (ugly, breaks at 3+ levels).- Application-side loops (slow, hard to debug).- Manual Excel lookups (error-prone, not scalable).
With recursive CTEs, you:- Traverse any depth of hierarchy in a single query.- Answer questions in seconds that would take hours manually.- Impress stakeholders with interactive org charts or exploded BOMs in dashboards.
Real-world impact:- HR: Org charts, headcount rollups, succession planning.- Manufacturing: Cost rollups, supply chain dependencies.- E-commerce: Category trees, product bundles.- Finance: Ownership structures, legal entity hierarchies.
If you ignore recursive CTEs, you’re either: - Writing brittle code that breaks when the hierarchy grows.- Exporting data to Python/R for processing (slow, not real-time).- Telling your boss, "I can’t do that in SQL."
UNION ALL
UNION
ALL
WHERE
WHERE manager_id IS NULL
LEVEL
DEPTH
WHERE depth < 5
CYCLE
CYCLE employee_id SET is_cycle USING path
employee_id | manager_id
employee_id | path
/1/4/7/
We’ll: 1. Create a sample employees table.2. Write a recursive CTE to show the full reporting chain for any employee.3. Extend it to count direct/indirect reports.
employees
-- PostgreSQL/SQL Server/MySQL 8.0+ CREATE TABLE employees ( employee_id INT PRIMARY KEY, name VARCHAR(100), manager_id INT REFERENCES employees(employee_id), salary DECIMAL(10, 2) ); -- Insert sample data (CEO -> VPs -> Managers -> ICs) INSERT INTO employees VALUES (1, 'Alice (CEO)', NULL, 200000), (2, 'Bob (VP Eng)', 1, 150000), (3, 'Carol (VP Sales)', 1, 140000), (4, 'Dave (Eng Manager)', 2, 120000), (5, 'Eve (Sales Manager)', 3, 110000), (6, 'Frank (Dev)', 4, 90000), (7, 'Grace (Dev)', 4, 95000), (8, 'Heidi (Sales)', 5, 85000), (9, 'Ivan (Sales)', 5, 80000);
Goal: Show the full reporting chain for Frank (employee_id = 6).
Frank (employee_id = 6)
WITH RECURSIVE org_chart AS ( -- Anchor member: Start with Frank SELECT employee_id, name, manager_id, 0 AS depth, ARRAY[employee_id] AS path -- PostgreSQL: track path as array FROM employees WHERE employee_id = 6 -- Frank UNION ALL -- Recursive member: Join to manager SELECT e.employee_id, e.name, e.manager_id, oc.depth + 1, oc.path || e.employee_id -- Append to path FROM employees e JOIN org_chart oc ON e.employee_id = oc.manager_id ) SELECT employee_id, name, manager_id, depth, path FROM org_chart ORDER BY depth DESC; -- Show from top (CEO) to bottom (Frank)
Expected Output:
employee_id | name | manager_id | depth | path ------------+-----------------+------------+-------+----------- 1 | Alice (CEO) | NULL | 2 | {6,4,1} 4 | Dave (Eng Mgr) | 2 | 1 | {6,4} 6 | Frank (Dev) | 4 | 0 | {6}
What’s happening:1. Anchor: Starts with Frank (employee_id = 6).2. Recursive step: Joins Frank to his manager (Dave), then Dave to his manager (Bob), then Bob to his manager (Alice).3. Termination: Stops when manager_id IS NULL (Alice has no manager).
employee_id = 6
manager_id IS NULL
Goal: For Bob (VP Eng), count: - Direct reports (Dave).- Indirect reports (Frank, Grace).
Bob (VP Eng)
WITH RECURSIVE reports AS ( -- Anchor: Bob's direct reports SELECT employee_id, name, manager_id, 1 AS depth FROM employees WHERE manager_id = 2 -- Bob UNION ALL -- Recursive: Their reports SELECT e.employee_id, e.name, e.manager_id, r.depth + 1 FROM employees e JOIN reports r ON e.manager_id = r.employee_id ) SELECT COUNT(*) AS total_reports, SUM(CASE WHEN depth = 1 THEN 1 ELSE 0 END) AS direct_reports, SUM(CASE WHEN depth > 1 THEN 1 ELSE 0 END) AS indirect_reports FROM reports;
total_reports | direct_reports | indirect_reports --------------+----------------+----------------- 3 | 1 | 2
Problem: What if Frank accidentally reports to Grace, who reports to Frank? Infinite loop.
Frank
Grace
Solution: Use CYCLE detection (PostgreSQL only):
WITH RECURSIVE org_chart AS ( SELECT employee_id, name, manager_id, 0 AS depth, ARRAY[employee_id] AS path FROM employees WHERE employee_id = 6 UNION ALL SELECT e.employee_id, e.name, e.manager_id, oc.depth + 1, oc.path || e.employee_id FROM employees e JOIN org_chart oc ON e.employee_id = oc.manager_id WHERE NOT e.employee_id = ANY(oc.path) -- Prevent cycles ) SELECT * FROM org_chart;
For MySQL/SQL Server: Add a depth limit (e.g., WHERE depth < 10).
depth
WHERE depth < 10
sql CREATE INDEX idx_employees_manager_id ON employees(manager_id);
WITH RECURSIVE org_chart AS (...)
WITH RECURSIVE cte1 AS (...)
ORDER BY depth DESC
SELECT * FROM org_chart
CONNECT BY
hierarchyid
depth < N
CREATE INDEX idx_employees_manager_id ON employees(manager_id)
JOIN
ON e.employee_id = oc.manager_id
Answer: Use a recursive CTE with UNION ALL.
"How would you count all direct and indirect reports for a manager?"
depth = 1
depth > 1
Answer: Recursive CTE with CASE WHEN depth = 1 THEN 1 ELSE 0 END.
CASE WHEN depth = 1 THEN 1 ELSE 0 END
"What’s the difference between UNION and UNION ALL in a recursive CTE?"
Answer: Always use UNION ALL.
"How would you prevent infinite loops in a recursive CTE?"
Challenge:Write a recursive CTE to calculate the total cost of a product’s Bill of Materials (BOM), including all subcomponents. Assume: - Table bom has columns: part_id, subcomponent_id, quantity.- Table parts has columns: part_id, cost.
bom
part_id
subcomponent_id
quantity
parts
cost
Example Data:
CREATE TABLE parts (part_id INT PRIMARY KEY, cost DECIMAL(10, 2)); INSERT INTO parts VALUES (1, 100), (2, 20), (3, 5), (4, 1); CREATE TABLE bom (part_id INT, subcomponent_id INT, quantity INT); INSERT INTO bom VALUES (1, 2, 2), -- Product 1 requires 2 of Part 2 (2, 3, 4), -- Part 2 requires 4 of Part 3 (3, 4, 10); -- Part 3 requires 10 of Part 4
Goal: Calculate the total cost of Product 1, including all subcomponents.
Product 1
WITH RECURSIVE bom_cost AS ( -- Anchor: Start with Product 1's direct components SELECT b.subcomponent_id AS part_id, p.cost, b.quantity, 1 AS depth FROM bom b JOIN parts p ON b.subcomponent_id = p.part_id WHERE b.part_id = 1 -- Product 1 UNION ALL -- Recursive: Get subcomponents of subcomponents SELECT b.subcomponent_id, p.cost, b.quantity * bc.quantity, -- Multiply quantities bc.depth + 1 FROM bom b JOIN bom_cost bc ON b.part_id = bc.part_id JOIN parts p ON b.subcomponent_id = p.part_id ) SELECT SUM(cost * quantity) AS total_cost FROM bom_cost;
Output:
total_cost ----------- 100 + (2 * 20) + (2 * 4 * 5) + (2 * 4 * 10 * 1) = 100 + 40 + 40 + 80 = 260
Why it works:- The anchor gets Product 1’s direct components (Part 2).- The recursive step multiplies quantities (e.g., 2 of Part 2 × 4 of Part 3 = 8 of Part 3).- SUM(cost * quantity) rolls up the total cost.
SUM(cost * quantity)
WITH RECURSIVE cte_name AS (...)
RECURSIVE
SELECT ... FROM table WHERE condition
SELECT ... FROM table JOIN cte_name ON ...
SELECT ... UNION ALL SELECT ...
0 AS depth
depth + 1
CYCLE part_id SET is_cycle USING path
CREATE INDEX idx_manager_id ON table(manager_id)
path VARCHAR(100)
b.quantity * bc.quantity
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.