Self-Join: Beginner
Two Rows, Same Table, Different Meaning
Recognize that questions comparing pairs within a table (manager/employee, previous/next, duplicate detection) require self-joins.
Here is the question that throws candidates the first time they see it: 'find every employee who earns more than their manager.' The employees table has employee_id and manager_id. There is no separate managers table. The candidate freezes for thirty seconds, then tries to write a CASE expression. The CASE expression cannot work, because comparing two rows in the same table requires the row twice. That is what a self-join is. You list the same table twice in the FROM clause, with different aliases, and join it to itself.
- ▸"employees who earn more than their manager"
- ▸"customers in the same city as another customer"
- ▸"find pairs of products that ..."
- ▸"detect duplicates where two rows share ..."
- ▸"compare each row to the previous one"
- ▸Any question where the comparison is between two rows of the same table
What the interviewer is actually testing
- ▸One table appears twice in the FROM clause with two different aliases
- ▸Each alias represents a different role (employee vs manager)
- ▸The join condition expresses the relationship between the two roles
- ▸The same row can play both roles in different result rows
Say the word 'self-join' out loud the moment you spot the pattern. The single utterance tells the interviewer you parsed the prompt correctly. Many candidates begin writing SQL before they name the technique, which forces the interviewer to figure out the approach from the syntax. Naming it first is what gets you credit for pattern recognition.
The 10-second decision
- Tries to write a single SELECT with a subquery in the WHERE
- Gets stuck because both rows are needed at once
- Asks 'is there a managers table?' (there is not)
- Loses two minutes to confusion before the right shape emerges
- Says 'this is a self-join, the employees table aliased twice'
- Writes FROM employees e JOIN employees m ON e.manager_id = m.employee_id
- Names the two aliases clearly: e for employee, m for manager
- Has working SQL on the page within ninety seconds
Why companies care
Aliasing the Table Twice (e and m)
Write a self-join with clear aliases and a precise ON clause that avoids Cartesian explosions.
The query you should be able to write from memory
- ▸e/m beats t1/t2: the reader sees "employee, manager" at a glance
- ▸parent/child for trees; prev/curr for sequences; before/after for snapshots
- ▸Aliasing is part of the answer; the interviewer reads it as part of the query
Why the aliases must be short and meaningful
In an interview, never name your aliases t1 and t2. Always pick aliases that hint at the row's semantic role. e/m, parent/child, before/after, left/right, prev/curr. When the interviewer reviews your query later, the aliases tell them you thought about the two roles separately, not just mechanically aliased to make the syntax work.
Walk through the trace, including the CEO question
Writing the Self-Join Condition
Prevent duplicate pairs using a < condition (a.id < b.id) and explain why to the interviewer.
The bug, stated plainly
Joining employees to themselves on department alone produces the cartesian product within each department. If a department has three employees, you get nine rows: Alice-Alice, Alice-Bob, Alice-Carol, Bob-Alice, Bob-Bob, Bob-Carol, Carol-Alice, Carol-Bob, Carol-Carol. The three self-pairs are nonsense. The six remaining rows are three real pairs counted twice in opposite orders. You need three rows out, you have nine, and the candidate who does not catch this returns nine.
The fix: the < condition
- ▸Self-pairs are dropped (the inequality is strict)
- ▸Each unordered pair appears exactly once
- ▸Generalizes to any symmetric comparison
- ▸Works on any unique column; employee_id, customer_id, transaction_id all qualify
- Returns 9 rows for a 3-person department
- Includes Alice-Alice, Bob-Bob, Carol-Carol
- Includes both (Alice, Bob) and (Bob, Alice)
- Symmetric pair counts inflate by a factor of 2
- Returns 3 rows for a 3-person department
- Drops self-pairs because the inequality is strict
- Keeps each unordered pair exactly once
- Generalizes to any symmetric comparison
When to use != vs <
Asking the interviewer 'are the pairs directional, or do we want each pair once?' is the move that earns the credit. Many candidates write the query and notice the duplicate problem when they trace the output. The candidates who ask first get full marks for the question without ever writing the wrong query.
"Earns More Than Their Manager"
Decide when LAG/LEAD or ROW_NUMBER replaces a self-join and when the self-join is the only clean option.
The two solutions, side by side
When the window function wins
When the self-join wins
- Comparison condition is not a fixed row offset
- The other row is matched by a join predicate, not by position
- The query needs to join on multiple columns simultaneously
- The engine has weak window-function support
- Comparison is 'previous row' or 'N rows ago' in a sorted partition
- The other row is at a fixed position relative to the current row
- Performance matters and you have a modern columnar engine
- The output should keep one row per input row, not produce pairs
Stating which tool you would reach for and why is what the interviewer scores. Many candidates can write both. Few can articulate the difference. The articulation is the answer; the SQL is just the proof.
| Situation | Phrasing that flatlines | Phrasing that lands |
|---|---|---|
| You see a same-table comparison | "I'll join the table to itself." | "This is a self-join. I'll alias employees as e for the employee and m for the manager; the join condition encodes the relationship between the two roles." |
| The interviewer asks 'find pairs in the same department' | "I'll join on department_id." | "Symmetric pairs need an inequality. e1.employee_id < e2.employee_id added to the ON clause drops self-pairs and keeps each unordered pair once." |
| The interviewer asks 'what about the CEO' | "They're not in the result." | "The inner join drops them because manager_id is NULL and NULL doesn't equal anything in SQL's three-valued logic. If the report should include the CEO, switch to LEFT JOIN and let m.name appear as NULL." |
| The interviewer asks 'now find each row vs the previous' | "I'll add a row-offset condition." | "For an adjacent-row comparison, LAG over a partition is cleaner than a self-join with a NOT EXISTS subquery. The self-join wins when the comparison isn't 'previous row' but 'a row defined by a join predicate.'" |
| The interviewer asks for 'all reports, any depth' | "I'll chain joins." | "Self-join is fixed-depth. Unbounded hierarchy needs a recursive CTE: anchor on direct reports, recursive step joins reports back to employees to walk one more level, terminates when the recursive step returns zero rows." |
Why a Subquery Won't Capture Two Roles
Handle recursive manager chains, multi-level comparisons, and know when to pivot to a recursive CTE.
- ▸Fixed depth (1-3 levels named in the prompt) → chained self-join
- ▸Unbounded depth (entire subtree, arbitrary depth) → recursive CTE
- ▸WITH RECURSIVE walks one level per iteration until no new rows
- ▸Most modern engines support it; mention the dialect constraint on older MySQL
The two-level self-join
When to reach for the recursive CTE
What the interviewer is testing at this level
- The depth is fixed and small (one or two levels)
- The question explicitly names the levels (manager, manager's manager)
- You want a flat row with columns for each level
- The engine is old and may not support recursive CTEs
- The depth is unbounded or unknown ahead of time
- The output is one row per descendant, with a depth column
- The traversal needs to compute aggregates per level
- The engine supports WITH RECURSIVE
The closing you can memorize
> You are in a Snowflake data engineering phone screen. The interviewer asks: 'Find every employee who earns more than their manager.'
e for employee and m for manager tell the interviewer you separated the two roles, while t1 and t2 work mechanically and read as anonymous.e1.employee_id < e2.employee_id to the ON clause: the strict inequality kills self-pairs and keeps each unordered pair exactly once. Use != only when the question wants both directions.LAG or LEAD on adjacent-row comparisons, where a self-join needs a NOT EXISTS subquery to isolate the immediately preceding row. Keep the self-join for comparisons defined by a condition rather than by row distance.RECURSIVE CTE with an anchor query and a step that joins the previous level back to the table.The table joins itself when the question compares rows to other rows
- Category
- SQL
- Difficulty
- beginner
- Duration
- 25 minutes
- Challenges
- 0 hands-on challenges
Topics covered: Two Rows, Same Table, Different Meaning, Aliasing the Table Twice (e and m), Writing the Self-Join Condition, "Earns More Than Their Manager", Why a Subquery Won't Capture Two Roles
Lesson Sections
- Two Rows, Same Table, Different Meaning (concepts: sqlSelfJoin)
What the interviewer is actually testing This question screens whether you can hold a small but unfamiliar shape in your head. The candidate who pauses, says 'I need to join the employees table to itself, with one alias for the employee and one for the manager,' has already cleared the bar. The candidate who tries to twist this into a single-table query with subqueries is signaling that they have not internalized that a row can play two roles at once. The interviewer is watching whether you reac
- Aliasing the Table Twice (e and m) (concepts: sqlSelfJoin)
The query has the same shape every time. Same table in the FROM clause twice, with two different aliases. Join condition expresses the relationship between the two roles. SELECT pulls columns from both aliases. The aliases are not decoration; they are the only way SQL knows which copy of the table you are talking about in each column reference. The query you should be able to write from memory Read this query out loud while you type it. The FROM clause lists employees twice. Alias e is the emplo
- Writing the Self-Join Condition (concepts: sqlSelfJoin)
The interviewer will give you a question that sounds slightly different: 'find pairs of employees in the same department.' Now the comparison is symmetric. Alice paired with Bob is the same pair as Bob paired with Alice. If you write the obvious join, you get every pair twice, plus every employee paired with themselves. The fix is a single inequality in the join condition. Most candidates miss it the first time. The interviewer is watching for it. The bug, stated plainly The fix: the < condition
- "Earns More Than Their Manager" (concepts: sqlSelfJoin)
The interviewer's follow-up: 'now compare each row to the previous one.' Two solutions work. The self-join, with the inequality moved into the join condition. The window function, with LAG over the ordered partition. Knowing both, and knowing when each wins, is the move. The two solutions, side by side Question: for each transaction, return the previous transaction's amount for the same account. Self-join solution: join transactions to itself on account_id, with the second copy filtered to the i
- Why a Subquery Won't Capture Two Roles (concepts: sqlRecursiveCte)
The interviewer escalates: 'now find every employee under a given manager, recursively, including indirect reports.' A plain self-join can only walk one level. Two levels takes two joins. Three levels takes three joins. Recursive depth takes a recursive CTE. The question is testing whether you know the boundary between a self-join and a recursive query, and when to cross it. The two-level self-join If the question stops at two levels (manager and the manager's manager), chain two self-joins. The