Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2 and Python 3.14.8
  • By MySmartCoPilot
Free intro, Pro: 7 of 30 minutes free
Free: Two problems on one org chart · The brute force, and why it fails · What to notice. With a pass: the other 10 sections and 3 interview answers, about 23 minutes.

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.

An org chart of nine employees over four levels, with one employee at the top and one whose manager id points at nobody.Asha (no manager)BilalChitraDevEshaFarhanGauriHarshJayamanager_id 99: nobody

The org chart the problems run on

Text description of the diagram

The chart has four levels and nine employees.

  1. Asha is at the top and has no manager, so her manager_id is NULL.
  2. Asha manages Bilal and Chitra.
  3. Bilal manages Dev and Esha; Chitra manages Farhan.
  4. Esha manages Gauri and Harsh, which is the fourth level.
  5. 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.

A join to the manager, and three joins for three levels SQL · sqlite/brute_force.sql
-- 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

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.id readable.
  • 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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.