RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
SQL questions
MediumSQL#467 min readJul 11, 2026

SELF JOIN

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What a self join really is

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.

How it works under the hood

  1. The SQL parser reads the table name twice and creates two logical references because the aliases are different.
  2. The optimizer chooses a join strategy, usually nested loop (compare each row to matching rows), hash join (build a hash table for one side), or merge join (walk sorted inputs together).
  3. For each row on the left side, SQL evaluates the join condition against candidate rows on the right side.
  4. If the condition is true, the pair is returned as one result row. If you use INNER JOIN, unmatched rows are dropped. If you use LEFT JOIN, unmatched left rows stay and the right side becomes NULL.
  5. The final output may contain more rows than the original table if one row matches many rows on the other side.

When and why to use it

  • Hierarchies: employee to manager, folder to parent folder, category to parent category.
  • Peer comparisons: find rows in the same table that share a value, such as duplicate orders or same-city customers.
  • Pairing rows: compare one row against another row to find relationships like older vs newer records.

Self join vs other ways to solve the problem

ApproachBest forWatch out for
Self joinSame-table relationshipsAliases, duplicates, NULLs
Correlated subquerySmall lookupsCan be slower and harder to read
Window functionRanking or adjacent row logicDoes not create row pairs directly
Recursive CTEMulti-level treesBetter 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.

Performance and edge cases

  • Time complexity: conceptually, a naive nested-loop self join is O(n²). Real databases usually do better by using indexes and smarter join algorithms.
  • Indexes matter: if you join on manager_id or another foreign key, an index on that column can cut work dramatically. Without it, the engine may scan many rows.
  • Memory use: hash joins can use extra memory roughly proportional to one side of the join, often O(n).
  • NULL behavior: 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.
  • Duplicate explosions: if you join on a non-unique column like city or department, one row may match many rows and create a large result set.
  • Symmetric pairs: if you want unique pairs like A-B but not B-A, add an ordering rule such as 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.

Real-world story

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.

SQL
-- 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:

  • How do you get every employee with their manager name? Use a 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.
  • How do you find duplicate rows in the same table? Self join the table on the columns that should be unique, then filter to the matched pairs. If you only want one copy of each pair, add an ordering condition such as t1.id < t2.id.
  • How is a self join different from a recursive CTE? A self join is one hop: row to row. A recursive CTE is for repeated hops, such as walking from employee to manager to director to VP across many levels.
  • Why do we need aliases in a self join? Because SQL must know which role each reference plays. Without aliases, the column names would collide and the query would be ambiguous.
  • What happens if one row matches many rows? You get one output row for every match, so the result can grow quickly. That is normal join behavior, but it can surprise people who expect only one output row per input row.
  • Why does the CEO disappear in an inner self join? Because the CEO usually has NULL in the manager column, and NULL does not equal any value, including another NULL.
  • Does a self join create a physical copy of the table? No. It creates two logical references, and the optimizer decides how to read the data efficiently.
  • Can a self join accidentally join a row to itself? Yes, if your join condition allows it. For peer matching, add a rule like t1.id <> t2.id or t1.id < t2.id depending on the business rule.

Common Mistakes:

  • Forgetting aliases. Correction: always give each side a different alias so the query is readable and unambiguous.
  • Using INNER JOIN when you need all rows. Correction: switch to LEFT JOIN if parentless rows must stay in the result.
  • Expecting NULL values to match. Correction: SQL does not treat NULL as equal to NULL with =.
  • Creating duplicate pairs accidentally. Correction: for symmetric pairs, use an ordering rule like 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:

  • Self join = same table joined to itself with aliases.
  • Use it for parent-child, manager-employee, or peer comparison.
  • INNER JOIN removes unmatched rows; LEFT JOIN keeps them.
  • Indexes on the join key help a lot on large tables.
  • For many levels of hierarchy, prefer a recursive CTE.
  • Watch for NULL, duplicates, and accidental self-matches.

Practice Tasks:

  • Write a self join that shows each employee and their manager.
  • Modify it to keep employees with no manager and label them No manager.
  • Find pairs of products in the same category, but return each pair only once.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

-- 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;