Hook: Interviewers love this one because it checks whether you can turn a simple manager-to-employee table into a full org chart, not just one level of reporting.
Question: How do I return an employee tree, including each person’s chain of managers and subordinates, using SQL?
Answer: Use a recursive CTE, which is a common table expression that refers to itself. Start with the top-level employees as the anchor rows, then repeatedly join each found manager to their direct reports until no new rows appear. This gives you the whole hierarchy with a depth level and, if you want, a readable path.
Interview-Ready Answer: I’d use a recursive CTE. First I select the root employees where manager_id is NULL, then I union a recursive step that joins each row to its direct reports using employee_id = manager_id. I usually carry a depth column and a path string so the result is easy to sort and read. The important detail is UNION ALL, because I want every discovered row and I do not want duplicate elimination unless I truly need it.
Detailed Explanation: A recursive CTE is the SQL version of “keep walking downward one level at a time.” The first query gets the starting point, and the second query uses the rows from the first pass to find the next pass. In org-chart terms, the first pass finds the CEO or department heads, and later passes find their reports, then the reports of those reports.
manager_id IS NULL.employees.manager_id = tree.employee_id.depth column, you can see the level number; if you include a path, you can sort or display the tree more clearly.UNION ALL matters| Choice | What it does | Why it matters |
|---|---|---|
UNION ALL | keeps every row | fastest and correct for trees |
UNION | removes duplicates | extra work; can hide valid rows |
For an employee tree, UNION ALL is the normal choice because each person should appear once if your data is clean. UNION does duplicate checking, which adds cost and can change results if your query shape is not careful.
| Approach | Best for | Main drawback |
|---|---|---|
| Recursive CTE | Any depth | Needs recursive support |
| Self-join chain | Known depth | Hard to extend |
With an index on manager_id, the recursive join can be very efficient because each level can find children quickly. Without that index, the database may scan the employee table again and again, and the query can drift toward quadratic behavior on big data. In practical terms, a 10,000-row org chart is usually fine with indexing; 100,000 rows is still common, but you should test it and watch the execution plan.
Also watch for dialect differences: SQL Server has a default recursion cap of 100 levels unless you change it, while PostgreSQL and SQLite allow recursive CTEs without that same small built-in cap. That matters if you have very deep hierarchies.
Memory of the mechanism: think of a flashlight shining from the top of the org chart downward, one layer at a time. The light starts at the roots, then spreads to children, then to grandchildren.
Real-World Example: Imagine an internal HR directory or approval system. When a manager opens a team page, the app needs the manager, all direct reports, all indirect reports, and sometimes the full chain above a person for approvals. A recursive CTE is a natural fit because the depth is unknown and changes as the company grows.
What goes wrong when people misunderstand it? A common bug is writing only one self-join, which returns direct reports but silently drops everyone deeper in the tree. In production, that looks like incomplete org charts, missing approvers, and access rules that seem to skip entire teams. Another failure mode is bad data with a cycle; then the query can become slow or hit the recursion limit, and logs may show repeated recursion warnings or timeouts while users see spinners or empty pages.
That is why interviewers like this question: it tests both SQL skill and whether you think about data quality, indexing, and termination conditions, not just syntax.
-- Recursive Employee Tree example
-- This script is self-contained: it creates sample data, builds the tree,
-- and also shows an edge case query for orphan employees.
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
employee_name VARCHAR(100) NOT NULL,
manager_id INTEGER NULL
);
INSERT INTO employees (employee_id, employee_name, manager_id) VALUES
(1, 'CEO', NULL),
(2, 'VP Engineering', 1),
(3, 'VP Sales', 1),
(4, 'Engineering Manager', 2),
(5, 'Software Engineer', 4),
(6, 'Sales Rep', 3),
(7, 'Orphan Employee', 99); -- Edge case: manager_id points to a missing row
WITH RECURSIVE employee_tree AS (
-- Anchor member: start at the roots of the organization.
-- Roots are employees with no manager.
SELECT
employee_id,
employee_name,
manager_id,
0 AS depth,
CAST(employee_name AS VARCHAR(500)) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive member: find each person's direct reports.
-- Each pass adds the next level of the tree.
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
et.depth + 1 AS depth,
CAST(et.path || ' > ' || e.employee_name AS VARCHAR(500)) AS path
FROM employees e
JOIN employee_tree et
ON e.manager_id = et.employee_id
)
SELECT
employee_id,
employee_name,
manager_id,
depth,
path
FROM employee_tree
ORDER BY path;
-- Edge case / failure-path check:
-- Orphans do not appear in the tree because their manager row is missing.
-- This query helps you find bad data that would otherwise be invisible.
SELECT
e.employee_id,
e.employee_name,
e.manager_id
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.employee_id
WHERE e.manager_id IS NOT NULL
AND m.employee_id IS NULL;Follow-up & Tricky Questions:
employee_id = 2 and then recurse downward from there.0 AS depth in the anchor member and depth + 1 in the recursive member. That makes it easy to indent, sort, or limit levels.CEO > VP Engineering > Engineering Manager. That helps with debugging and stable ordering.UNION ALL preferred over UNION? Because a tree is supposed to emit every discovered row, and duplicate elimination is wasted work unless you are intentionally deduplicating a graph.manager_id points to a missing employee? That employee becomes an orphan and will not appear in the recursive tree. You need a separate query or a data-fix step to surface the problem.ORDER BY using depth or path if presentation matters.Common Mistakes:
UNION when UNION ALL is enough. Correction: UNION ALL is faster and usually the right choice for trees.Memory Hook: Roots first, then one branch at a time. Picture a tree drawn by dropping a pebble at the top; each ripple is the next level of reports.
Cheat Sheet:
WITH RECURSIVE = named query that can refer to itself.depth and path for readability.UNION ALL for hierarchy traversal.manager_id for speed.Practice Tasks: