SQL Self Join: How to Join a Table to Itself
SQL self join matches rows from a table with other rows in that same table. This example uses separate aliases to compare employees with their managers and shows how the join condition controls each match.
Updated · SQL Arena team
What you will practice
- Join one table to itself with clear aliases
- Match an employee row to a manager row
- Choose INNER JOIN or LEFT JOIN for self joins
- Build unique pairs with a.id < b.id
- Keep alias roles consistent across SELECT, ON, and WHERE
A practice problem in the trainer
This is how a problem looks in SQL Arena: the task, the editor, the result and the database schema.
In employees, manager_id points to another row's id. Return the employee, the direct manager, and the employee salary only when the employee earns more than that manager.
SELECT e.employee_name AS employee, m.employee_name AS manager, e.salary FROM employees e JOIN employees m ON e.manager_id = m.id WHERE e.salary > m.salary ORDER BY e.salary DESC, e.employee_name;
| # | employee | manager | salary |
|---|---|---|---|
| 1 | Quinn | Nora | 130000 |
| 2 | Nora | Maya | 125000 |
| 3 | Pia | Leo | 98000 |
In employees, manager_id points to another row's id. Return the employee, the direct manager, and the employee salary only when the employee earns more than that manager.
SELECT e.employee_name AS employee, m.employee_name AS manager, e.salary FROM employees e JOIN employees m ON e.manager_id = m.id WHERE e.salary > m.salary ORDER BY e.salary DESC, e.employee_name;
How an SQL Self Join Works
The query references employees twice. Alias e represents the employee row and alias m represents the manager row. They are not copies of the data; they are two roles assigned to the same table inside one query. Qualifying columns with aliases is essential here because names such as id, employee_name, and salary exist on both sides of the join.
The join condition e.manager_id = m.id follows the relationship stored in the table. If Leo has manager_id = 1, his row is matched with Maya's row where id = 1. A regular JOIN is appropriate because this task only needs employees who have a matching manager. If the report had to keep top-level employees with no manager, LEFT JOIN would preserve them and return NULL for manager columns.
Once the rows are matched, WHERE e.salary > m.salary compares values from the two roles. Quinn, Nora, and Pia pass that test in the sample data. The final ORDER BY sorts by employee salary and then by name, so the output is deterministic rather than dependent on whichever row order PostgreSQL happens to produce.
SELECT e.employee_name AS employee, m.employee_name AS manager, e.salary FROM employees e JOIN employees m ON e.manager_id = m.id WHERE e.salary > m.salary ORDER BY e.salary DESC, e.employee_name;
| employee | manager | salary |
|---|---|---|
| Quinn | Nora | 130000 |
| Nora | Maya | 125000 |
| Pia | Leo | 98000 |
Common SQL Self Join Mistakes
Joining id to id
A condition such as e.id = m.id only matches each employee to the same row. For an employee-manager hierarchy, join the relationship column to the referenced key: e.manager_id = m.id.
Reversing alias roles
Changing e.salary > m.salary to m.salary > e.salary answers a different question. Pick role-based aliases and keep them consistent throughout the query.
Creating mirrored pairs
For pair-generation problems, a.id <> b.id produces both A-B and B-A. Use a.id < b.id when the order of the two rows does not matter and each pair should appear once.
Practice problems on this topic
Interview preparation trackA useful progression is to move from hierarchy matching to pair generation. The free task about unique category-supplier pairs trains duplicate control, while the employee-versus-manager task trains a direct self-reference. Harder exercises extend the same idea to reporting structures, including direct-report maps and manager counts. If you are preparing for technical interviews, continue with the “Interview Preparation” track. SQL Arena contains 1,192 tasks, and answers are checked against hidden data rather than only the visible sample.
- EasyfreeUnique category-supplier pairspostgresql
- MediumfreeUnique Category–Supplier Pairs in the Mid Price Rangepostgresql
- EasyfreeEmployees earning more than their managerpostgresqlmysqlclickhouse
- EasyfreeCombined Client Balancepostgresqlmysqlclickhouse
- MediumPremiumTop 2 manufacturer-drug pairspostgresqlmysql
- HardPremiumThree-topping pizza combinationspostgresqlmysqlclickhouse
- ExpertPremiumHow many people report to a managerpostgresql
- MediumPremiumMap every direct reportpostgresql
Reference on this topic
In-depth articles with syntax and examples in the SQL Arena reference.
Frequently asked questions
What is an SQL self join?
An SQL self join joins a table to another logical instance of itself. Different aliases let the query treat rows from the same physical table as separate roles.
How do you self join a table in SQL?
Reference the table twice with different aliases and write an ON condition that connects the two roles. For an employee hierarchy, an employee's manager_id can be joined to another row's id.
Why are aliases required in an SQL self join?
Both sides expose columns with the same names, so aliases identify which logical instance each column belongs to. They also make the relationship between the two row roles explicit.
Can an SQL self join use LEFT JOIN?
Yes. LEFT JOIN is useful when rows without a matching related row should remain in the result, such as top-level employees who do not have a manager.
Practice SQL Self Join Queries
Work through self join tasks in SQL Arena, from employee-manager matching to duplicate-free pairs. The free plan includes 292 tasks across PostgreSQL, MySQL, ClickHouse, and BigQuery with no card required.
Practice self joinsMore practice from CodeRang
- Python ArenaPython Arena has 1300+ Python 3.12 problems covering functions, classes, async code and technical interview tasks.1300+ tasks · Python 3.12
- Playwright ArenaPlaywright Arena has 30 JavaScript and E2E problems where you write a test and immediately see what it did in the browser.30 tasks · JavaScript / E2E