Hook: A self join is like holding one table up to a mirror so one row can look at another row in the same data.
Question: What is a self join in SQL?
Answer: A self join is when you join a table to itself by using two different aliases, which are just temporary names for the same table. It is useful when rows in the same table have a relationship, like an employee pointing to a manager, or a category pointing to its parent. The database does not make a physical copy of the table; it just treats the table as two logical instances during the query.
Interview-Ready Answer: I use a self join when rows in one table need to be compared with other rows in that same table, like employee-to-manager or parent-to-child data. I write the table twice with aliases, then join on a relationship column such as employee.manager_id = manager.employee_id. The key detail is that the same table appears twice logically, and I often use a LEFT JOIN if I want to keep rows that have no matching parent, like a CEO with a NULL manager.
A self join is not a special join keyword. It is the normal join operation, but both sides of the join come from the same table. The important idea is role naming: one alias plays one role, and the other alias plays another role. For example, e can mean the employee row and m can mean the manager row, even though both come from the same employees table.
INNER JOIN, unmatched rows are dropped. If you use LEFT JOIN, unmatched left rows stay and the right side becomes NULL.| Approach | Best for | Watch out for |
|---|---|---|
| Self join | Same-table relationships | Aliases, duplicates, NULLs |
| Correlated subquery | Small lookups | Can be slower and harder to read |
| Window function | Ranking or adjacent row logic | Does not create row pairs directly |
| Recursive CTE | Multi-level trees | Better for deep hierarchies than repeated self joins |
Simple rule: use a self join for one hop, a recursive CTE for many hops, and a window function when you need order-based calculations rather than row-to-row matching.
manager_id or another foreign key, an index on that column can cut work dramatically. Without it, the engine may scan many rows.NULL = NULL is not true in SQL, so an inner self join will not match missing parents. Use LEFT JOIN if you want to keep top-level rows.t1.id < t2.id.Memory of the mechanism: the database is not “joining a table to itself” as a trick; it is joining role A to role B inside the same data.
Imagine a customer-support dashboard in a subscription billing system. The agents table stores each agent and a supervisor_id pointing to another row in the same table. A self join powers an org chart so the UI can show each agent with their supervisor name, team lead, and escalation path.
What goes wrong when someone uses the wrong join? A developer writes an INNER JOIN instead of a LEFT JOIN. Suddenly, new supervisors, contractors, and the top-level support director disappear from the dashboard because they do not have a matching parent row. The app still runs, so there is no obvious error, but the report undercounts staff, the org chart has holes, and managers think people were deleted. In logs you may only see a valid query and a smaller-than-expected row count, which is why self-join bugs can hide in plain sight.
Why interviewers care: this is a small query pattern that reveals whether you understand aliasing, join types, and how SQL treats missing matches.
-- Self join demo: one table, two roles, and a NULL edge case.
-- This script is intentionally simple and portable.
WITH employees(employee_id, employee_name, manager_id) AS (
SELECT 1, 'Ava', CAST(NULL AS INTEGER) UNION ALL
SELECT 2, 'Ben', 1 UNION ALL
SELECT 3, 'Cara', 1 UNION ALL
SELECT 4, 'Dan', 2 UNION ALL
SELECT 5, 'Eli', 2 UNION ALL
SELECT 6, 'Fay', CAST(NULL AS INTEGER)
)
-- INNER JOIN keeps only employees who have a matching manager row.
SELECT
e.employee_id,
e.employee_name AS employee,
m.employee_name AS manager
FROM employees e
JOIN employees m
ON e.manager_id = m.employee_id
ORDER BY e.employee_id;
WITH employees(employee_id, employee_name, manager_id) AS (
SELECT 1, 'Ava', CAST(NULL AS INTEGER) UNION ALL
SELECT 2, 'Ben', 1 UNION ALL
SELECT 3, 'Cara', 1 UNION ALL
SELECT 4, 'Dan', 2 UNION ALL
SELECT 5, 'Eli', 2 UNION ALL
SELECT 6, 'Fay', CAST(NULL AS INTEGER)
)
-- LEFT JOIN keeps top-level rows like Ava and Fay even when manager_id is NULL.
SELECT
e.employee_id,
e.employee_name AS employee,
COALESCE(m.employee_name, 'No manager') AS manager
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.employee_id
ORDER BY e.employee_id;Follow-up & Tricky Questions:
LEFT JOIN from employee to manager so rows with NULL manager IDs still appear, and use COALESCE if you want a friendly label like No manager.t1.id < t2.id.NULL in the manager column, and NULL does not equal any value, including another NULL.t1.id <> t2.id or t1.id < t2.id depending on the business rule.Common Mistakes:
INNER JOIN when you need all rows. Correction: switch to LEFT JOIN if parentless rows must stay in the result.NULL values to match. Correction: SQL does not treat NULL as equal to NULL with =.id1 < id2.Memory Hook: Think “mirror + two names”: one table, two roles, one relationship. If you can name the two roles, you can write the join.
Cheat Sheet:
INNER JOIN removes unmatched rows; LEFT JOIN keeps them.NULL, duplicates, and accidental self-matches.Practice Tasks:
No manager.