SQL (PostgreSQL, MySQL, SQLite) Module 22 – SQL interview preparation
Org chart drills: self-joins and recursion
Two original org-chart problems: each employee beside their manager, and every manager with their whole team, from the level-by-level join to recursion.
What you will learn
- Compare rows of one table with a self-join that keeps employees without a manager
- Walk an org chart to any depth with a recursive common table expression
- Handle a missing manager and a cycle that would make a walk run for ever
Before you start
What you’ll learn · Before you start
What you will learn
- Compare rows of one table with a self-join that keeps employees without a manager
- Walk an org chart to any depth with a recursive common table expression
- Handle a missing manager and a cycle that would make a walk run for ever
Before you start
On this page
An org chart lives in one table that points at itself, and interviewers like it because two questions on the same nine rows need two different tools. Comparing an employee with their own manager is a self-join: the table twice, under two aliases. Reaching the whole team below somebody is recursion: a query that reads its own output until no new rows arrive. The data below has the two traps that separate a careful answer from a quick one, an employee with no manager and an employee whose manager left, and later a cycle that an import created.
Two problems on one org chart
The table employees(id, name, manager_id, hired_on, salary) holds nine employees. manager_id is the id of
another employee, NULL for the person at the top and, after a bad import, sometimes an id that no employee has.
The org chart the problems run on
Text description of the diagram
The chart has four levels and nine employees.
- Asha is at the top and has no manager, so her manager_id is NULL.
- Asha manages Bilal and Chitra.
- Bilal manages Dev and Esha; Chitra manages Farhan.
- Esha manages Gauri and Harsh, which is the fourth level.
- Jaya stands apart: her manager_id is 99, and no employee has that id, so the link is broken rather than absent.
One self-join reaches one level, so three joins are needed for the three levels below Asha, and a chart one level deeper would need a fourth. A recursive query walks every level with the same text.
Problem A: each employee beside their manager. Return every one of the nine employees with their manager’s name, and mark the two cases where there is none: NULL, and an id pointing at nobody. Then answer two questions on the same shape: who was hired before the person who now manages them, and who earns more than their manager.
Problem B: every manager with their whole team. For each employee who manages anybody, return how many employees are below them at any depth and what those employees cost in salary. The chart is four levels deep today and may grow. Sizes: up to 2 × 10⁵ employees, up to 12 levels.
The brute force, and why it fails
Both problems have an answer that uses joins only. The first is a plain JOIN to the manager; the second joins the
table to itself once per level.
-- Nine employees. Asha has no manager, Jaya's manager left and her manager_id points nowhere,
-- and two employees were hired before the person who now manages them.
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
manager_id INTEGER,
hired_on TEXT NOT NULL,
salary INTEGER NOT NULL
);
INSERT INTO employees VALUES
(1, 'Asha', NULL, '2019-04-01', 240000),
(2, 'Bilal', 1, '2020-07-15', 180000),
(3, 'Chitra', 1, '2018-02-20', 175000),
(4, 'Dev', 2, '2021-09-01', 120000),
(5, 'Esha', 2, '2019-11-11', 130000),
(6, 'Farhan', 3, '2022-03-07', 110000),
(7, 'Gauri', 5, '2023-06-19', 90000),
(8, 'Harsh', 5, '2024-01-08', 85000),
(9, 'Jaya', 99, '2022-12-01', 95000);
-- Problem A: every employee with their manager, and who was hired first.
-- A plain JOIN loses Asha, who has no manager, and Jaya, whose manager is missing.
SELECT e.name AS employee, m.name AS manager,
CASE WHEN e.hired_on < m.hired_on THEN 'before the manager' ELSE 'after the manager' END AS hired
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;
-- How many rows each form returns: 9 employees, 7 with a manager who exists.
SELECT (SELECT count(*) FROM employees) AS employees,
(SELECT count(*) FROM employees AS e JOIN employees AS m ON m.id = e.manager_id) AS with_inner_join,
(SELECT count(*) FROM employees AS e LEFT JOIN employees AS m ON m.id = e.manager_id) AS with_left_join;
-- Problem B, brute force: the whole team under each manager, found by joining level by level.
-- Three joins reach three levels down, and a fourth level would need a fourth join.
SELECT a.name AS manager,
count(DISTINCT b.id) + count(DISTINCT c.id) + count(DISTINCT d.id) AS reports_found
FROM employees AS a
LEFT JOIN employees AS b ON b.manager_id = a.id
LEFT JOIN employees AS c ON c.manager_id = b.id
LEFT JOIN employees AS d ON d.manager_id = c.id
GROUP BY a.id, a.name
HAVING reports_found > 0
ORDER BY a.id; Output
┌──────────┬─────────┬────────────────────┐ │ employee │ manager │ hired │ ├──────────┼─────────┼────────────────────┤ │ Bilal │ Asha │ after the manager │ │ Chitra │ Asha │ before the manager │ │ Dev │ Bilal │ after the manager │ │ Esha │ Bilal │ before the manager │ │ Farhan │ Chitra │ after the manager │ │ Gauri │ Esha │ after the manager │ │ Harsh │ Esha │ after the manager │ └──────────┴─────────┴────────────────────┘ ┌───────────┬─────────────────┬────────────────┐ │ employees │ with_inner_join │ with_left_join │ ├───────────┼─────────────────┼────────────────┤ │ 9 │ 7 │ 9 │ └───────────┴─────────────────┴────────────────┘ ┌─────────┬───────────────┐ │ manager │ reports_found │ ├─────────┼───────────────┤ │ Asha │ 7 │ │ Bilal │ 4 │ │ Chitra │ 1 │ │ Esha │ 2 │ └─────────┴───────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < brute_force.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
The first result has seven rows, not nine: the plain JOIN silently drops Asha, who has no manager, and Jaya, whose
manager_id is 99. The counts confirm it, 7 against 9. The level-by-level answer to problem B gets the right
numbers here, but only because three joins happen to reach the bottom of a four-level chart: one more level and it
quietly undercounts, and the query has to be rewritten whenever the company reorganises. It also double-counts
nothing only because count(DISTINCT …) is used per level.
What to notice
- A self-join needs aliases, because both sides are the same table, and the alias is what makes
e.manager_id = m.idreadable. - Keep the rows you are asked about. The question says “every employee”, so the join to the manager is a
LEFT JOIN, and a NULL manager and a missing one are different answers. - One join reaches one level. A question with “at any depth”, “the whole team” or “the chain to the top” needs recursion, not more joins.
Problem A: the self-join, with every employee kept Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Problem B: recursion, and what each walk costs
- The cycle: reproduced, guarded, and found in the data
- The same walks in PostgreSQL and MySQL
- The index, and what each query reads
- Five drafts that return the wrong rows
- Variants an interviewer adds next
- Your turn
- Every employee with their manager and their own team size
- Check yourself
- Interview questions
- List every employee with their manager's name from employees(id, name, manager_id). The company has one employee at the top, and after an HR import some manager_id values point at employees who are no longer in the table. Write it, then say how many rows each version returns.
- A dashboard needs, for every manager in a 200,000-employee org chart about twelve levels deep, the number of employees below them at any depth and their total salary. Write the query and say what it costs.
- After an HR import, the query that shows each employee's chain of managers hangs on PostgreSQL and fails on MySQL with "Recursive query aborted after 1001 iterations". Explain both, fix the query, and find the bad data.
- Key takeaways
7 runnable examples (SQL, hierarchy.py, mysql_server.py, pg_server.py) · 1 exercise with sample tests · 3 interview answers · about 25 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- PostgreSQL documentation: WITH Queries (recursive queries, SEARCH and CYCLE) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Joins Between Tables (self-joins) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Table Expressions (join types and aliases) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Subquery Expressions (IN, NOT IN, EXISTS) (The PostgreSQL Global Development Group)
- PostgreSQL 14 release notes (SEARCH and CYCLE clauses) (The PostgreSQL Global Development Group)
- SQLite: The WITH Clause (recursive common table expressions) (SQLite)
- SQLite: The SELECT statement (joins and aliases) (SQLite)
- SQLite: Built-in Scalar SQL Functions (instr) (SQLite)
- SQLite: EXPLAIN QUERY PLAN (SQLite)
- MySQL 8.4 Reference Manual: WITH (Common Table Expressions) (Oracle Corporation)
- MySQL 8.4 Reference Manual: Server System Variables (cte_max_recursion_depth) (Oracle Corporation)
- MySQL 8.4 Reference Manual: JOIN Clause (Oracle Corporation)
- MySQL 8.4 Reference Manual: Problems with NULL Values (Oracle Corporation)
- Oracle AI Database SQL Language Reference: Hierarchical Queries (CONNECT BY) (Oracle Corporation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress