SQL self join: join a table with itself
Sometimes the relationship lives inside a single table. In the employes (employees) table, the manager_id column holds the identifier of… another employee. To show “Léa reports to Hugo”, you need to join the employes table with itself: that is a self join.
Syntax
SELECT e.nom, m.nom AS manager
FROM employes e
LEFT JOIN employes m ON m.id = e.manager_id;How it works
A self join has no special keyword: it is a regular JOIN where the same table appears twice. To tell them apart, aliases are required: here e is the employee and m their manager.
A LEFT JOIN is often used, to keep the rows that have no “parent” (the CEO has no manager).
Self joins are also used to compare rows with each other: employees who earn more than their manager, products in the same category, customers from the same city…
Diagram
| id | nom | manager_id |
|---|---|---|
| 1 | Durand | NULL |
| 2 | Martin | 1 |
| 3 | Roux | 2 |
| employee | manager |
|---|---|
| Durand | NULL |
| Martin | Durand |
| Roux | Martin |
Simplified diagram to show the idea. The examples below use the real SQL Quest practice database (a fictional online shop: see the full schema).
Glossary: the sample database uses French names
clients- customers
commandes- orders
lignes_commande- order lines
produits- products
fournisseurs- suppliers
employes- employees
prenom / nom- first name / last name (or name)
ville- city
pays- country
prix- price
prix_unitaire- unit price
quantite- quantity
categorie- category
date_commande- order date
poste- job title
departement- department
salaire- salary
statut- status: livrée = delivered, expédiée = shipped, en cours = in progress, annulée = cancelled
Examples with their results
1. Each employee and their manager
The CEO appears with a NULL manager thanks to the LEFT JOIN.
SELECT e.prenom || ' ' || e.nom AS employe, e.poste,
m.prenom || ' ' || m.nom AS manager
FROM employes e
LEFT JOIN employes m ON m.id = e.manager_id
LIMIT 8; Try it in the sandbox| employe | poste | manager |
|---|---|---|
| Chloé Dubois | PDG | NULL |
| Jade Lefebvre | Directeur commercial | Chloé Dubois |
| Nathan Roux | Directrice technique | Chloé Dubois |
| Samir Simon | Responsable RH | Chloé Dubois |
| Alice Simon | Commercial | Jade Lefebvre |
| Nathan Martin | Commercial | Jade Lefebvre |
| Lucas Moreau | Commercial | Jade Lefebvre |
| Emma Lambert | Commercial | Jade Lefebvre |
8 rows in total
2. Count the direct reports
How many people does each manager lead directly?
SELECT m.prenom, m.nom, m.poste, COUNT(e.id) AS equipe
FROM employes m
JOIN employes e ON e.manager_id = m.id
GROUP BY m.id
ORDER BY equipe DESC; Try it in the sandbox| prenom | nom | poste | equipe |
|---|---|---|---|
| Jade | Lefebvre | Directeur commercial | 12 |
| Samir | Simon | Responsable RH | 5 |
| Nathan | Roux | Directrice technique | 4 |
| Chloé | Dubois | PDG | 3 |
4 rows in total
3. Compare two rows
The employees who earn more than half of their manager's salary.
SELECT e.nom, e.salaire, m.nom AS manager, m.salaire AS salaire_manager
FROM employes e
JOIN employes m ON m.id = e.manager_id
WHERE e.salaire > m.salaire / 2
LIMIT 8; Try it in the sandbox| nom | salaire | manager | salaire_manager |
|---|---|---|---|
| Lefebvre | 6,200 | Dubois | 9,500 |
| Roux | 6,800 | Dubois | 9,500 |
| Simon | 4,800 | Dubois | 9,500 |
| Simon | 4,800 | Lefebvre | 6,200 |
| Martin | 3,400 | Lefebvre | 6,200 |
| Moreau | 4,600 | Lefebvre | 6,200 |
| Lambert | 4,200 | Lefebvre | 6,200 |
| Lefebvre | 3,800 | Lefebvre | 6,200 |
8 rows in total
Common mistakes
Forgetting the aliases
Without aliases, SQL cannot tell which “copy” of the table each column belongs to.
SELECT nom, nom FROM employes JOIN employes ON id = manager_id;SELECT e.nom, m.nom FROM employes e JOIN employes m ON m.id = e.manager_id;Reversing the condition
e.id = m.manager_id gives the direct reports instead of the managers. Read the condition again: the manager's id must equal the employee's manager_id.
SELECT e.nom, m.nom AS manager FROM employes e JOIN employes m ON e.id = m.manager_id;SELECT e.nom, m.nom AS manager FROM employes e JOIN employes m ON m.id = e.manager_id;Losing the top of the hierarchy
With an INNER JOIN, the employee without a manager disappears. Use LEFT JOIN to keep them.
SELECT e.nom, m.nom FROM employes e JOIN employes m ON m.id = e.manager_id;SELECT e.nom, m.nom FROM employes e LEFT JOIN employes m ON m.id = e.manager_id;Exercises
Try writing the query yourself before opening the solution.
Challenge 1. Show the employees (first name, last name) whose manager is the “Directeur commercial” (sales director).
Show a solution
SELECT e.prenom, e.nom FROM employes e JOIN employes m ON m.id = e.manager_id WHERE m.poste = 'Directeur commercial';| prenom | nom |
|---|---|
| Alice | Simon |
| Nathan | Martin |
| Lucas | Moreau |
| Emma | Lambert |
| Alice | Lefebvre |
| Inès | Mercier |
| Louis | Bernard |
| Louis | Garcia |
12 rows in total · 8 shown
Try it in the sandboxChallenge 2. Show each manager and the average salary of their team.
Show a solution
SELECT m.nom, ROUND(AVG(e.salaire)) AS salaire_moyen_equipe FROM employes m JOIN employes e ON e.manager_id = m.id GROUP BY m.id;| nom | salaire_moyen_equipe |
|---|---|
| Dubois | 5,933 |
| Lefebvre | 4,417 |
| Roux | 4,250 |
| Simon | 4,120 |
4 rows in total
Try it in the sandboxThe interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.
Frequently asked questions
Is there a SELF JOIN keyword?
No. A self join is written with an ordinary JOIN where the same table appears twice, with two different aliases.
How do I show the whole multi-level hierarchy?
With a recursive CTE (WITH RECURSIVE), which repeats the self join level by level.
Is a self join slow?
No more than a join between two different tables. An index on the linking column (here manager_id) speeds it up on large tables.