Step 1 Understand it
Why you need it. manager_id points into the same employees . To show "Hana reports to Ben", you need Hana's and Ben's row side by side. There is only one table, so you use it twice.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- One table, two aliases: e for the employee, m for the manager. Treat them as two copies of employees.
- Hana's manager_id is 2. In the m copy, emp_id 2 is Ben. So the result row is Hana → Ben.
- Every employee is matched to their manager the same way. Asha's manager_id is NULL, so she matches nobody; LEFT JOIN keeps her anyway.
- The result: each employee beside their manager. With a plain JOIN, Asha would vanish.
Picture it
flowchart LR
subgraph E["employees as e"]
H["Hana · manager_id 2"]
B1["Ben · manager_id 1"]
end
subgraph M["employees as m"]
B2["2 · Ben"]
A["1 · Asha"]
end
H -->|"m.emp_id = e.manager_id"| B2
B1 --> AThe idea
Give the table two and treat them as two copies: e for the employee, m for the manager. Join on m.emp_id = e.manager_id, meaning "the manager copy's id equals the employee copy's manager_id". Now each result row has the employee's and their manager's columns. Use a if you want to keep Asha, who has no manager. Self also compare rows with each other — pairs in the same department, or rows on consecutive days.
The shape of it
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.emp_id = e.manager_id;Words in lower case are the parts you fill in.
How it works, step by step
- e and m are both the employees table.
- For Hana (manager_id 2), find the m row with emp_id 2: Ben.
- The result row holds e.name = Hana and m.name = Ben.
- Asha's manager_id is , so she has no match; LEFT JOIN keeps her with a NULL manager.
- To compare rows, add a condition:
a.emp_id < b.emp_idmakes each pair appear once and stops a row pairing with itself.
Analogy
Two photocopies of the staff list on your desk. On the left copy you find Hana and read "manager: 2". On the right copy you find person 2 — Ben — and write his name next to hers.
Where the analogy breaks
The photocopies are real copies. doesn't copy anything: the aliases are just two names for the same table, which is why you must prefix every column (e.name, m.name) — otherwise "name" is ambiguous.
Worked examples
Each employee and their manager
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.emp_id = e.manager_id
ORDER BY e.emp_id;8 rows
| employee | manager |
|---|---|
| Asha | NULL |
| Ben | Asha |
| Chen | Asha |
| Dara | Asha |
| Eli | Dara |
| Fatima | Asha |
| Gus | Dara |
| Hana | Ben |
Pairs of colleagues in the same department
SELECT a.name AS first, b.name AS second, a.dept_id
FROM employees a
JOIN employees b ON b.dept_id = a.dept_id AND a.emp_id < b.emp_id
ORDER BY a.dept_id, a.name, b.name;7 rows
| first | second | dept_id |
|---|---|---|
| Asha | Ben | 1 |
| Asha | Chen | 1 |
| Asha | Hana | 1 |
| Ben | Chen | 1 |
| Ben | Hana | 1 |
| Chen | Hana | 1 |
| Dara | Eli | 2 |
a.emp_id < b.emp_id keeps each pair once (Asha–Ben, never also Ben–Asha) and never pairs someone with themselves.
How many people report to each manager
SELECT m.name AS manager, COUNT(*) AS direct_reports
FROM employees e
JOIN employees m ON m.emp_id = e.manager_id
GROUP BY m.emp_id, m.name
ORDER BY direct_reports DESC, manager;3 rows
| manager | direct_reports |
|---|---|
| Asha | 4 |
| Dara | 2 |
| Ben | 1 |
Must-know points
- A uses the same table twice with two aliases.
- Read the condition carefully:
m.emp_id = e.manager_idmeans "m is e's manager". - Use LEFT JOIN to keep rows whose pointer is NULL (the top of the tree).
a.id < b.idgives each pair once;a.id <> b.idgives each pair twice.- A self join reaches one level. For a whole chain of managers, use a (Phase 4).
Common mistakes
- Swapping the condition (
e.emp_id = m.manager_id), which lists each person's reports instead of their manager. - Forgetting prefixes, so
nameis ambiguous. - Using and losing the CEO.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Read
short page with a try-it editor, free
-
Practice
free exercises
-
Practice
PostgreSQL Exercises — Joins and Subqueries
pgexercises.com, free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain what
m.emp_id = e.manager_idmeans in plain words.Show answer
Read the employees table twice, as two copies: e for the employee, m for the manager.
m.emp_id = e.manager_idsays: "for this employee e, find the row in m whose id is the number written in e's manager_id".For Hana, manager_id is 2, so the match is the row with emp_id 2: Ben. So m.name is her manager's name.
-
Writefrom a blank page, then run it
Write a showing each employee, their manager, and their manager's manager (two self joins).
Show answer
Join once to get the manager (m), then join again from the manager to their manager (mm). Use LEFT JOINs so people at the top still show, with NULLs: Asha has no manager, and the people under Asha have no manager's manager.
sqlSELECT e.name AS employee, m.name AS manager, mm.name AS managers_manager FROM employees e LEFT JOIN employees m ON m.emp_id = e.manager_id LEFT JOIN employees mm ON mm.emp_id = m.manager_id ORDER BY e.emp_id;8 rows
employee manager managers_manager Asha NULL NULL Ben Asha NULL Chen Asha NULL Dara Asha NULL Eli Dara Asha Fatima Asha NULL Gus Dara Asha Hana Ben Asha -
Predictsay it before you run it
How many rows does the first example return if LEFT JOIN becomes JOIN? Who is missing?
Show answer
7 rows instead of 8. Asha is missing: her manager_id is NULL, so she has no matching manager row, and an INNER JOIN drops her.
sqlSELECT e.name AS employee, m.name AS manager FROM employees e JOIN employees m ON m.emp_id = e.manager_id ORDER BY e.emp_id;7 rows
employee manager Ben Asha Chen Asha Dara Asha Eli Dara Fatima Asha Gus Dara Hana Ben -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Find employees hired before their manager. Who has no manager, and what does your query do with them?
Show answer
Join each employee to their manager, then compare the two hire dates:
WHERE e.hire_date < m.hire_date.In this data the answer is nobody: no rows. Every manager joined before the people under them. (Asha joined in 2019, Dara and Ben in 2020, and everyone they manage later.) An empty result is a real answer; don't "fix" the query until it returns something.
Asha has no manager. The INNER JOIN drops her before the runs. With a LEFT JOIN she would reach the WHERE, but
e.hire_date < NULLis unknown, so she would be dropped anyway. Either way, she is never counted as "hired before her manager", which is right.sqlSELECT e.name, e.hire_date, m.name AS manager, m.hire_date AS manager_hired FROM employees e JOIN employees m ON m.emp_id = e.manager_id WHERE e.hire_date < m.hire_date ORDER BY e.name;No rows
name hire_date manager manager_hired
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
A self join joins a table to itself with two aliases so I can compare rows within it — the classic case is employees and managers through a manager_id. I join the manager alias's to the employee alias's manager_id, use a LEFT JOIN if the top of the hierarchy must appear, and for pair I add id less-than id to avoid self-pairs and mirrored duplicates.
Follow-up questions
How would you find employees who earn more than their manager?
Self join e to m on m.emp_id = e.manager_id, then WHERE e.salary > m.salary. Here it returns Hana.
How do you get the whole chain of managers, not just one level?
A recursive CTE that starts from the employee and repeatedly joins to the manager until manager_id is NULL.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.