SELF JOIN — Joining a Table with Itself
Join a Table to Itself
A school leader table stores both staff and their supervisors. The report needs a person’s name beside that person’s supervisor.
A self join uses separate aliases for two references to the same table. Each alias represents a different role in the relationship.
Explain and apply
Give one table two roles and use their relationship to match students to mentors.
Work it out
Show each practice student beside the student with the preceding roll number, retaining the first student.
Avoid mistakes
A self join is not inherently a recursive search.
Join a Table to Itself: the request and its result
School need
A school leader table stores both staff and their supervisors. The report needs a person’s name beside that person’s supervisor.
Database operation
A self join uses separate aliases for two references to the same table. Each alias represents a different role in the relationship.
Check the outcome
Both references read the same stored table. Their aliases assign different roles, and the left join keeps Aarav even though roll number zero is absent.
Understand what the database does
The first table reference represents the person and the second represents the supervisor. Match the person’s supervisor key with the supervisor’s own key. A LEFT JOIN preserves a person with no supervisor; an INNER JOIN would omit that person.
Related concepts: Name Result Columns and Recursive Queries.
What is SELF JOIN?
A SELF JOIN is a regular join, but the table is joined with itself. We use table aliases to differentiate between the "left" and "right" references.
Syntax
SQL · portable concepts; preserved examples identify their engine
SELECT a.column1, b.column2
FROM table_name a
JOIN table_name b
ON a.common_column = b.common_column
AND a.column1 <> b.column1;
Sample Table – students
SQL · portable concepts; preserved examples identify their engine
CREATE TABLE students (
roll_no INT PRIMARY KEY,
name VARCHAR(50),
class VARCHAR(10),
city VARCHAR(30)
);
INSERT INTO students VALUES
(1, 'Aarav Sharma', '10A', 'Delhi'),
(2, 'Diya Iyer', '9B', 'Chennai'),
(3, 'Sneha Patil', '10A', 'Pune'),
(4, 'Mehul Agarwal', '8C', 'Hyderabad'),
(5, 'Saira Bano', '10A', 'Delhi');
1. Find Students from the Same Class
Input definitions and data for this example. The shown statements establish the base fixture. Where this section changes an existing object, its required object definition is included below; use the stated before/after table to follow a sequence of changes.
CREATE TABLE students (
roll_no INT PRIMARY KEY,
name VARCHAR(50),
class VARCHAR(10),
city VARCHAR(30)
);
INSERT INTO students VALUES
(1, 'Aarav Sharma', '10A', 'Delhi'),
(2, 'Diya Iyer', '9B', 'Chennai'),
(3, 'Sneha Patil', '10A', 'Pune'),
(4, 'Mehul Agarwal', '8C', 'Hyderabad'),
(5, 'Saira Bano', '10A', 'Delhi');
SQL · portable concepts; preserved examples identify their engine
Let’s list pairs of students who are in the same class but are not the same person.
SELECT a.name AS student_1, b.name AS student_2, a.class
FROM students a
JOIN students b
ON a.class = b.class
AND a.roll_no <> b.roll_no
ORDER BY a.name, b.name;
student_1 | student_2 | class
----------------+----------------+-------
Aarav Sharma | Sneha Patil | 10A
Aarav Sharma | Saira Bano | 10A
Sneha Patil | Aarav Sharma | 10A
Sneha Patil | Saira Bano | 10A
Saira Bano | Aarav Sharma | 10A
Saira Bano | Sneha Patil | 10A
Notice the duplicate pairs (e.g., Aarav–Sneha and Sneha–Aarav). We can fix that in the next step.
2. Avoiding Duplicate Pairs
Input definitions and data for this example. The shown statements establish the base fixture. Where this section changes an existing object, its required object definition is included below; use the stated before/after table to follow a sequence of changes.
CREATE TABLE students (
roll_no INT PRIMARY KEY,
name VARCHAR(50),
class VARCHAR(10),
city VARCHAR(30)
);
INSERT INTO students VALUES
(1, 'Aarav Sharma', '10A', 'Delhi'),
(2, 'Diya Iyer', '9B', 'Chennai'),
(3, 'Sneha Patil', '10A', 'Pune'),
(4, 'Mehul Agarwal', '8C', 'Hyderabad'),
(5, 'Saira Bano', '10A', 'Delhi');
SQL · portable concepts; preserved examples identify their engine
To avoid repeating the same pair in reverse, use < on roll numbers:
SELECT a.name AS student_1, b.name AS student_2, a.class
FROM students a
JOIN students b
ON a.class = b.class
AND a.roll_no < b.roll_no;
student_1 | student_2 | class
----------------+----------------+-------
Aarav Sharma | Sneha Patil | 10A
Aarav Sharma | Saira Bano | 10A
Sneha Patil | Saira Bano | 10A
3. Use Case – Students from the Same City
Input definitions and data for this example. The shown statements establish the base fixture. Where this section changes an existing object, its required object definition is included below; use the stated before/after table to follow a sequence of changes.
CREATE TABLE students (
roll_no INT PRIMARY KEY,
name VARCHAR(50),
class VARCHAR(10),
city VARCHAR(30)
);
INSERT INTO students VALUES
(1, 'Aarav Sharma', '10A', 'Delhi'),
(2, 'Diya Iyer', '9B', 'Chennai'),
(3, 'Sneha Patil', '10A', 'Pune'),
(4, 'Mehul Agarwal', '8C', 'Hyderabad'),
(5, 'Saira Bano', '10A', 'Delhi');
SQL · portable concepts; preserved examples identify their engine
Want to find students living in the same city (excluding self-pairs)?
SELECT a.name AS student_1, b.name AS student_2, a.city
FROM students a
JOIN students b
ON a.city = b.city
AND a.roll_no < b.roll_no;
student_1 | student_2 | city
----------------+----------------+--------
Aarav Sharma | Saira Bano | Delhi
Best Practices
Query discussed in this section:
SELECT a.name AS student_1, b.name AS student_2, a.city
FROM students a
JOIN students b
ON a.city = b.city
AND a.roll_no < b.roll_no;
- Always use aliases (like a and b) in SELF JOINs to distinguish the table references.
- Use < or > on IDs to avoid symmetric duplicates.
- Use meaningful filters in the ON clause to prevent large result sets.
Important differences and mistakes
A self join is not inherently a recursive search. One join follows one relationship level; deeper chains need repeated joins or a recursive query. Use aliases consistently so a comparison does not accidentally compare a row with itself.
Try it with the school data
Show each practice student beside the student with the preceding roll number, retaining the first student.
This practice uses a separate, fresh fixture named practice_students. It does not continue the changing table state of the earlier examples.
| roll_no | name | class | marks | city |
|---|---|---|---|---|
| 1 | Aarav | 10A | 85 | Pune |
| 2 | Diya | 9B | 70 | Chennai |
| 3 | Sneha | 10A | 85 | Pune |
SQL · create only these practice tables in a disposable database:
CREATE TABLE practice_students (roll_no INTEGER PRIMARY KEY, name VARCHAR(50), class VARCHAR(10), marks INTEGER, city VARCHAR(30));
INSERT INTO practice_students VALUES
(1, 'Aarav', '10A', 85, 'Pune'),
(2, 'Diya', '9B', 70, 'Chennai'),
(3, 'Sneha', '10A', 85, 'Pune');
Worked answer and reasoning
This practice uses a separate, fresh fixture named practice_students. It does not continue the changing table state of the earlier examples.
| roll_no | name | class | marks | city |
|---|---|---|---|---|
| 1 | Aarav | 10A | 85 | Pune |
| 2 | Diya | 9B | 70 | Chennai |
| 3 | Sneha | 10A | 85 | Pune |
SQL · create only these practice tables in a disposable database:
CREATE TABLE practice_students (roll_no INTEGER PRIMARY KEY, name VARCHAR(50), class VARCHAR(10), marks INTEGER, city VARCHAR(30));
INSERT INTO practice_students VALUES
(1, 'Aarav', '10A', 85, 'Pune'),
(2, 'Diya', '9B', 70, 'Chennai'),
(3, 'Sneha', '10A', 85, 'Pune');
SQL · this practice variation runs with SQLite 3; the preserved examples retain their stated dialect
SELECT child.name AS student, previous.name AS previous_student FROM practice_students AS child LEFT JOIN practice_students AS previous ON previous.roll_no = child.roll_no - 1 ORDER BY child.roll_no;| student | previous_student |
|---|---|
| Aarav | NULL |
| Diya | Aarav |
| Sneha | Diya |
Both references read the same stored table. Their aliases assign different roles, and the left join keeps Aarav even though roll number zero is absent.
Check your understanding
Use the idea independently
A self join uses separate aliases for two references to the same table. Each alias represents a different role in the relationship.
Both references read the same stored table. Their aliases assign different roles, and the left join keeps Aarav even though roll number zero is absent.
Continue your understanding with Name Result Columns and Recursive Queries.
Source for the engine-specific behavior: MySQL 8.4 reference documentation.
Reaching the recap and completing the quizzes are separate milestones.