Imagine an org chart where one table has to act like both the employee list and the manager list. Interviewers love this question because it checks whether you can use the same table twice without mixing up the roles.
Question: How do you list each employee together with their manager’s name?
Answer: Use a self join, which means joining a table to itself using two aliases. One alias represents the employee row, and the other represents the manager row, matched by employee.manager_id = manager.employee_id. A LEFT JOIN is usually safest because it keeps top-level employees, like the CEO, even when they have no manager.
Interview-Ready Answer: I’d use a self join on the employee table. I’d alias it once as e for the employee and once as m for the manager, then join e.manager_id to m.employee_id. I usually pick LEFT JOIN so root employees still appear with a null manager, which is important if the org chart starts at the CEO.
Detailed Explanation: A self join is not a special SQL feature; it is just a normal join where the same table appears twice in the query. The only trick is using aliases, which are short names that let SQL tell the two copies apart. Think of it as putting the same company directory on two desks: one desk is labeled “employee,” the other is labeled “manager.”
Ben with manager_id = 1.employee_id matches the employee’s manager_id.LEFT JOIN, employees with no manager still stay in the result, and the manager columns become NULL.LEFT JOIN is usually the better defaultAn INNER JOIN only returns rows where both sides match. That is fine for strict reporting, but it silently removes the top of the hierarchy, because the CEO often has NULL in manager_id. A LEFT JOIN keeps the employee rows even when there is no manager row to match.
| Method | What it returns | Best for |
|---|---|---|
| INNER JOIN | Only matched rows | Employees who must have managers |
| LEFT JOIN | All employees | Org charts and reports |
| Recursive CTE | All levels | Full reporting chains |
If you only need the immediate manager, a self join is the simplest answer. If you need the full chain, such as employee → manager → director → VP, you need a recursive CTE (a query that repeatedly feeds its own output back into itself until no more rows are found).
In interview terms, say this: “Self join for one hop; recursive CTE for the whole tree.” That sentence is a strong signal that you understand the difference.
Logically, a self join is still one join. In real systems, performance depends on indexes and the optimizer. If employee_id is a primary key and manager_id is indexed, the database can usually match rows quickly; without an index on manager_id, it may scan a large part of the table, which can turn a millisecond query into a seconds-long query on a million-row table.
Rule of thumb: the output size is at most one row per employee for the immediate-manager report, so the result is usually O(n) in rows returned. The hidden cost is the lookup work, which is why indexing manager_id matters so much.
manager_id is NULL, so only LEFT JOIN keeps the row.manager_id points to a missing employee; the manager columns come back NULL.employee_id.Real-World Example: In an HR dashboard for a SaaS company, the nightly report shows every employee, their direct manager, and the manager’s department. The analytics team used an INNER JOIN at first, and the CEO’s row vanished because the CEO had no manager. The report looked “almost right,” which is exactly why the bug was dangerous.
What went wrong: headcount totals no longer matched payroll, and the support team saw blank manager columns for a few new hires whose manager_id had not been loaded yet. The logs showed fewer rows than the employee table, but no query error was raised, so the issue slipped through until an executive noticed their own row was missing from the org chart. The fix was to switch to a LEFT JOIN, surface null manager names clearly, and add a data-quality check for orphaned manager_id values.
-- One-table org chart using a self join.
-- The sample data includes two edge cases:
-- 1) a top-level employee with no manager (NULL)
-- 2) a bad row whose manager_id points to nobody (99)
-- COALESCE returns the first non-NULL value, so the report stays readable.
WITH employees (employee_id, employee_name, manager_id) AS (
VALUES
(1, 'Asha', CAST(NULL AS INTEGER)),
(2, 'Ben', 1),
(3, 'Chen', 1),
(4, 'Dina', 2),
(5, 'Eli', 99)
)
SELECT
e.employee_id,
e.employee_name AS employee,
COALESCE(m.employee_name, '[No manager found]') AS manager
FROM employees AS e
LEFT JOIN employees AS m
ON e.manager_id = m.employee_id
ORDER BY e.employee_id;Follow-up & Tricky Questions:
LEFT JOIN instead of INNER JOIN? Because org charts usually include root employees with no manager. LEFT JOIN keeps those rows, while INNER JOIN silently drops them.GROUP BY the manager row and count employee IDs. That gives a simple direct-report count per manager.manager_id helps the lookup from employee to manager, while employee_id is usually already indexed as a primary key.LEFT JOIN, show the missing manager as NULL or a label like [No manager found], and fix the source data separately.Common Mistakes:
INNER JOIN by default. Correction: use LEFT JOIN when you must keep top-level employees.e for employee and m for manager.employee_id and manager_id.Memory Hook: “Same table, two hats.” One alias wears the employee hat, the other wears the manager hat.
Cheat Sheet:
e.manager_id = m.employee_id.LEFT JOIN keeps CEOs and orphan rows visible.manager_id for faster lookups.Practice Tasks:
LEFT JOIN.INNER JOIN and notice which rows disappear.