Phase 3 · Joins: combining tables · Topic 3.4

Self joins: a table joined to itself

Must know

A self join joins a table to itself under two different aliases, so a row can be compared with another row of the same table — such as an employee with their manager.

Not started

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

AnimationA self join compares a table with itself
Read the animation as text
  1. One table, two aliases: e for the employee, m for the manager. Treat them as two copies of employees.
  2. Hana's manager_id is 2. In the m copy, emp_id 2 is Ben. So the result row is Hana → Ben.
  3. 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.
  4. 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 --> A
The same table plays two parts. Each employee row (e) is matched with the row of its manager (m) by comparing m.emp_id with e.manager_id.

The 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

sql
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

  1. e and m are both the employees table.
  2. For Hana (manager_id 2), find the m row with emp_id 2: Ben.
  3. The result row holds e.name = Hana and m.name = Ben.
  4. Asha's manager_id is , so she has no match; LEFT JOIN keeps her with a NULL manager.
  5. To compare rows, add a condition: a.emp_id < b.emp_id makes 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

sql
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

employeemanager
AshaNULL
BenAsha
ChenAsha
DaraAsha
EliDara
FatimaAsha
GusDara
HanaBen

Pairs of colleagues in the same department

sql
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

firstseconddept_id
AshaBen1
AshaChen1
AshaHana1
BenChen1
BenHana1
ChenHana1
DaraEli2

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

sql
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

managerdirect_reports
Asha4
Dara2
Ben1

Must-know points

  • A uses the same table twice with two aliases.
  • Read the condition carefully: m.emp_id = e.manager_id means "m is e's manager".
  • Use LEFT JOIN to keep rows whose pointer is NULL (the top of the tree).
  • a.id < b.id gives each pair once; a.id <> b.id gives 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 name is 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.

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.

  1. ExplainSay what it does and when to use it, in plain words, with no notes.

    Explain what m.emp_id = e.manager_id means 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_id says: "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.

  2. 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.

    sql
    SELECT 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

    employeemanagermanagers_manager
    AshaNULLNULL
    BenAshaNULL
    ChenAshaNULL
    DaraAshaNULL
    EliDaraAsha
    FatimaAshaNULL
    GusDaraAsha
    HanaBenAsha
  3. 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.

    sql
    SELECT 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

    employeemanager
    BenAsha
    ChenAsha
    DaraAsha
    EliDara
    FatimaAsha
    GusDara
    HanaBen
  4. 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 < NULL is unknown, so she would be dropped anyway. Either way, she is never counted as "hired before her manager", which is right.

    sql
    SELECT 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

    namehire_datemanagermanager_hired

Practice box: write and run your answers here

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.

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.