Contribute OA questions
OAHelper
CompaniesProblemsTopicsInterview Experiences
Explore
Week 01

Week 01

Self Joins, Anti-Joins & Existence Checks

Day 52 hrs

SELECT, WHERE, ORDER BY & LIMITAggregation, GROUP BY & HAVINGCASE WHEN, Conditional Aggregation & PivotsINNER & LEFT JOIN: Combining TablesSelf Joins, Anti-Joins & Existence ChecksSubqueries, CTEs & Set Operations

What to Focus On

Some questions need two rows from the same table: an employee and their manager, or two customers from the same city. Others ask for a missing relationship: a customer with no orders or a product with no sales.

You already know the join syntax. Here the work is choosing what each side represents, avoiding duplicate pairs, and handling NULL correctly when checking for a missing match.

We will use an employee table first, then customers and orders. Pay particular attention to the difference between NOT EXISTS and NOT IN; a nullable column can change the result completely.

employees

idnamemanager_idsalary
1AnjaliNULL190000
2Rohit1145000
3Meera1120000
4Vikram2160000
5Farhan295000
6Divya3120000
7Sneha4175000

manager_id is not a name, it is the id of another row in this same table. Anjali is the founder, so hers is NULL, SQL's way of saying "no value here".


Use the Same Table in Two Roles

To compare an employee's salary with their manager's, we need both records on the same result row. A self join uses employees twice: e for the employee and m for the manager. These aliases describe two roles in the query; they do not create copies of the stored table.

Step 1: just pair each person with their boss

Check the relationship first: e.manager_id should match the manager's m.id.

SQL
SELECT e.name AS employee, m.name AS manager
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;
employeemanager
RohitAnjali
MeeraAnjali
VikramRohit
FarhanRohit
DivyaMeera
SnehaVikram

7 employees went in, 6 rows came out. Anjali is missing, and that is not a bug.

Step 2: pull the salaries onto the same row

The pairing works, so add the two numbers you want to compare.

SQL
SELECT e.name AS employee, e.salary AS emp_salary,
       m.name AS manager,  m.salary AS mgr_salary
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;
employeeemp_salarymanagermgr_salary
Rohit145000Anjali190000
Meera120000Anjali190000
Vikram160000Rohit145000
Farhan95000Rohit145000
Divya120000Meera120000
Sneha175000Vikram160000

The comparison is now a plain left versus right check on one row. The two salaries are now available for comparison.

Step 3: keep only the rows you were asked for

Now filter on the two salary columns.

SQL
SELECT e.name AS employee, e.salary AS emp_salary,
       m.name AS manager,  m.salary AS mgr_salary
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
WHERE e.salary > m.salary
ORDER BY e.name;
employeeemp_salarymanagermgr_salary
Sneha175000Vikram160000
Vikram160000Rohit145000

Two names, sorted alphabetically. The requested output is alphabetical, so we order by employee name.

Read the query line by line.

  1. FROM employees AS e refers to the employee whose salary we are checking.
  2. JOIN employees AS m refers to the manager's row in the same table.
  3. ON m.id = e.manager_id attaches the manager row whose id matches.
  4. WHERE e.salary > m.salary keeps employees who earn more than their manager.
  5. SELECT ... picks the four columns and renames them with AS.
  6. ORDER BY e.name sorts the survivors before the judge sees them.

Two people are missing, and both reasons matter. Divya earns 120000 and her manager Meera also earns 120000. > is strict, so equal is not more and Divya is out. Had the statement said "at least as much as", >= would bring her back.

Anjali produces no row at all. Her manager_id is NULL, and a comparison against NULL is never true, so the ON line finds nothing and the JOIN drops her. Here that is correct, she has no manager to out-earn.

  1. 1employees as e and m
  2. 2match ON e.manager_id = m.id
  3. 3compare salaries
  4. 4keep pairs where e.salary
  5. 5m.salary
Check what you learned

Using the employees table above, a student writes WHERE e.salary >= m.salary for the ask "employees earning strictly more than their manager". What extra name appears?

When the question says "show everyone"

Sometimes the ask is different: "list every employee with their manager's name, print No Manager when there is none". Now Anjali must appear. Swap the JOIN for a LEFT JOIN, which keeps every left row and pads the right side with NULL.

Two small tools help. COALESCE(a, b) returns a unless it is NULL, in which case it returns b, so it replaces a blank with a label. CASE WHEN ... THEN ... ELSE ... END is SQL's if else, one value per row.

SQL
SELECT e.name AS EmployeeName,
       COALESCE(m.name, 'No Manager') AS ManagerName,
       CASE WHEN m.id IS NOT NULL AND e.salary > m.salary
            THEN 'Yes' ELSE 'No' END AS EarnsMore
FROM employees AS e
LEFT JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;
EmployeeNameManagerNameEarnsMore
AnjaliNo ManagerNo
RohitAnjaliNo
MeeraAnjaliNo
VikramRohitYes
FarhanRohitNo
DivyaMeeraNo
SnehaVikramYes

All 7 rows survive, and Anjali's blank manager is now a readable label.

Note the m.id IS NOT NULL guard inside the CASE. Without it Anjali's comparison is 190000 > NULL, which is unknown, so the ELSE branch fires and she gets No. Right answer, wrong reason, and the variant asking for "Not Applicable" then fails.


What is an Anti-Join?

For a question like "customers who never ordered", we need to retain customers for whom no matching order exists.

An anti-join is exactly that: the rows of A that have no partner in B. "Customers who never ordered." "Products never sold."

In SQLite, write this using NOT EXISTS or LEFT JOIN ... IS NULL. You may also see NOT IN, but its behaviour differs when the subquery contains NULL.

customers

idnamecity
1AshaBengaluru
2BilalHyderabad
3ChitraPune
4DeepakBengaluru
5NithyaChennai

orders

idcustomer_idamount
1011420
1021610
1031275
104NULL150
1053330
1065480

Order 104 is a guest checkout, so its customer_id is NULL. Keep it in mind when comparing NOT IN with NOT EXISTS.


The Pattern

  1. Decide the direction. Whichever table's rows must survive even when nothing matches goes in FROM.
  2. LEFT JOIN the other table on the key.
  3. Filter with WHERE <right table>.<never null column> IS NULL, which keeps exactly the padded rows.
  4. Choose that column carefully: the primary key is safe, a nullable column like amount is not.
  5. Aggregate afterwards if the ask is "how many", then ORDER BY exactly as stated.

Step 1: inspect the join before filtering. Check which rows the final filter should retain.

SQL
SELECT c.id, c.name, o.id AS o_id, o.customer_id AS o_cust
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
ORDER BY c.id, o.id;
c.idc.nameo_ido_cust
1Asha1011
1Asha1021
1Asha1031
2BilalNULLNULL
3Chitra1053
4DeepakNULLNULL
5Nithya1065

Bilal and Deepak are the padded rows, where LEFT JOIN had nothing to attach and filled in NULL. They are the answer, already visible. Asha appears three times because she placed three orders, and order 104 appears nowhere, since a NULL key matches no customer.

Step 2: keep only the padded rows.

SQL
SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.name;
name
Bilal
Deepak

One column, two rows, alphabetical. That is the anti-join.

IS NULL is a separate operator on purpose: you cannot write o.id = NULL, because NULL is not a value you can be equal to.

Dry run of the same query, step by step.

stepwhat runsrows alive
FROM + LEFT JOINpair each customer with her orders, pad the unmatched7
WHERE o.id IS NULLkeep only the padded rows2
SELECT c.nameproject one column2
ORDER BY c.nameBilal, then Deepak2

The join is the widest point of the query, and the filter does all the work, only because that one column makes the padded rows recognisable.

Check the output shape. This query returns one name column, ordered alphabetically:

Text
name
Bilal
Deepak

Remove debugging columns before submitting, and follow the ordering and formatting the question specifies.

What the wrong column looks like. Suppose a payment is pending, so order 105 has amount stored as NULL. A student tests the wrong column:

SQL
SELECT c.name FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.amount IS NULL          -- wrong column
ORDER BY c.name;
wrong outputcorrect output
BilalBilal
ChitraDeepak
Deepak

Chitra has an order, but its missing amount causes the filter to include her. A sample with all amounts filled will not expose this mistake. Test a column guaranteed to be non-null on a real match, usually the child table's primary key.

Check what you learned

Which column is guaranteed non-null on every real order row, making it a reliable way to identify the padded rows?


Query Templates

SQL

SQL
-- 1. Self join: compare a row against its parent row in the same table.
SELECT e.name AS employee
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
WHERE e.salary > m.salary
ORDER BY e.name;

-- 2. LEFT self join: every employee, including the ones with no manager.
SELECT e.name AS EmployeeName,
       COALESCE(m.name, 'No Manager') AS ManagerName,
       CASE WHEN m.id IS NOT NULL AND e.salary > m.salary
            THEN 'Yes' ELSE 'No' END AS EarnsMore
FROM employees AS e
LEFT JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;

-- 3. Anti-join, the safe form: rows of A with no partner in B.
SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.name;

-- 4. Semi-join: rows of A that DO have a partner, no duplicates, no DISTINCT needed.
SELECT c.name
FROM customers AS c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)
ORDER BY c.name;

Variations

Variation 1 - Anti-join three ways

The NOT EXISTS subquery checks whether a matching order exists for the current customer. This is the logical check; the database may optimise how it performs it.

SQL
-- (b) NOT EXISTS: correct, NULL safe
SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)
ORDER BY c.name;

-- (c) NOT IN: WRONG whenever the subquery column can be NULL
SELECT c.name FROM customers c
WHERE c.id NOT IN (SELECT customer_id FROM orders)
ORDER BY c.name;

Form (a) is the LEFT JOIN ... IS NULL you already wrote. Run all three on the tables above:

formoutput
LEFT JOIN, o.id IS NULLBilal, Deepak
NOT EXISTSBilal, Deepak
NOT IN(no rows at all)

Two forms agree, one returns an empty file. The NOT IN version fails because the list contains NULL.

Why NOT IN collapses. The subquery produces (1, 1, 1, NULL, 3, 5). Ask SQL whether Bilal qualifies: is 2 NOT IN (1, 1, 1, NULL, 3, 5)?

SQL cannot say yes. That NULL is an unknown value, and for all SQL knows it is 2. So the answer is not true and not false, it is unknown. WHERE keeps only true rows, so every customer is thrown away.

Delete order 104 and the same query suddenly returns Bilal and Deepak. Test nullable keys explicitly; a sample without them can make the two forms look equivalent.

IMP

NULL check: If you use NOT IN here, exclude NULLs in the subquery: NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL). NOT EXISTS expresses the missing-match question directly. With plain IN, known matches remain true even if the list also contains NULL.

Variation 2 - Semi-join with EXISTS

The mirror image: rows of A that do have a partner. EXISTS stops at the first matching order, so Asha comes out once even though she has three orders.

SQL
SELECT c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)
ORDER BY c.name;
EXISTS output (correct)plain JOIN output
AshaAsha
ChitraAsha
NithyaAsha
Chitra
Nithya

The plain JOIN returns five rows because Asha has three orders. EXISTS returns each qualifying customer row once. DISTINCT on names gives the expected result here, but could merge different customers with the same name.

Variation 3 - Pairs from one table with a.id < b.id

"List every pair of customers in the same city." Join the table to itself on city and control the pairing with the id.

SQL
SELECT a.name AS customer_1, b.name AS customer_2, a.city
FROM customers a
JOIN customers b ON a.city = b.city AND a.id < b.id;
customer_1customer_2city
AshaDeepakBengaluru

Bengaluru has two customers, so exactly one pair comes out.

See what the wrong conditions produce:

condition on the ON linewhat you get
a.id < b.idAsha, Deepak. One row, correct
a.id <> b.idAsha, Deepak and Deepak, Asha. Same pair twice
nothing at all7 rows, including Asha paired with herself

< picks one ordering of each pair and prints it once. <> removes only the self pairs, not the mirror image.

Variation 4 - Consecutive rows by id

When ids are gap free, "customers with three consecutive orders" is a triple self join on id + 1 and id + 2.

SQL
SELECT DISTINCT o1.customer_id AS customer_id
FROM orders o1
JOIN orders o2 ON o2.id = o1.id + 1 AND o2.customer_id = o1.customer_id
JOIN orders o3 ON o3.id = o1.id + 2 AND o3.customer_id = o1.customer_id;
customer_id
1

Orders 101, 102 and 103 all belong to Asha, so only customer 1 qualifies.

DISTINCT is not optional. A run of five consecutive orders gives three starting positions for the same customer, so it prints three times. If the ids have gaps this approach is simply wrong, and you need LAG and LEAD, which come in Week 2.

Variation 5 - "Every customer here has ordered"

"Which cities have no inactive customer?" This is a double anti-join, with two absence checks: a city qualifies when there is no customer in it for whom there is no order.

SQL
SELECT DISTINCT c.city
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM customers c2
  WHERE c2.city = c.city
    AND NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c2.id))
ORDER BY c.city;
city
Chennai
Pune

Bengaluru is out because of Deepak, Hyderabad because of Bilal. Chennai and Pune have one customer each, and both ordered.

The counting version says it differently. GROUP BY collapses rows into one row per group, and HAVING filters those groups the way WHERE filters rows.

SQL
SELECT c.city
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.city
HAVING COUNT(DISTINCT CASE WHEN o.id IS NOT NULL THEN c.id END) = COUNT(DISTINCT c.id)
ORDER BY c.city;

Same two cities, comparing "customers here who ordered" against "customers here". Choose the version you can explain clearly. NOT EXISTS follows the wording closely here.

Check what you learned

Order 104 has customer_id set to NULL. What does SELECT name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders) return?


Common Mistakes

  • No aliases in a self join. Both sides expose the same column names. Give each role an alias and qualify the columns so the relationship is clear.
  • NOT IN over nullable values. A NULL in the inner list prevents the condition from being true for any customer here. Use NOT EXISTS, or exclude NULLs deliberately when NOT IN fits the question.
  • Testing a nullable data column. WHERE o.amount IS NULL also includes customers with an order whose amount is missing. Test o.id, which cannot be NULL on a real order.
  • Including equal salaries. “Strictly more” needs >. Using >= adds Divya even though she earns the same as her manager.
  • Losing employees with no matching manager. An inner join removes both NULL manager ids and ids that point to no existing row. Use LEFT JOIN when these employees must remain.
  • Returning each pair twice. a.id <> b.id removes self-pairs but keeps both directions. a.id < b.id returns each unordered pair once.
  • Repeating customers in a long run. Five consecutive orders have three starting positions for a three-order run. Use DISTINCT when the output needs each customer only once.
  • Assuming ids define sequence. Joining on id + 1 is suitable only when consecutive ids are what the question means. For events ordered by time, define the order explicitly and use the window-function approach covered later.

Interview Notes

When would you use a self join? When two rows of the same table play different roles, such as an employee and their manager. Explain the roles before writing the matching condition.

NOT IN or NOT EXISTS? For a missing relationship, NOT EXISTS is a clear default and handles nullable inner keys naturally. NOT IN can also be correct when NULLs are ruled out. Neither syntax is automatically faster; performance depends on the plan and available indexes.

Is LEFT JOIN ... IS NULL the same as NOT EXISTS? They express the same absence check when they use the same matching condition and the IS NULL test identifies only unmatched rows. A right-table primary key is a reliable column for that test.

EXISTS or IN? IN checks membership in the subquery result; EXISTS checks whether the subquery returns any row. Choose the form that matches the question. The engine may optimise either form, so do not assume IN always builds a full list or EXISTS is always faster.

How do you find duplicates? Group by the columns that define a duplicate and use HAVING COUNT(*) > 1. To show pairs of duplicate records, self join on those columns and use a.id < b.id to avoid mirror pairs.

How would you list employees with no reports? Use an anti-join against the same table: FROM employees e LEFT JOIN employees r ON r.manager_id = e.id WHERE r.id IS NULL. The result here is Divya, Farhan and Sneha.

How do you find cities where every customer has ordered? Look for cities with no customer who has no order. Double NOT EXISTS expresses that directly. The count comparison works too, provided multiple orders do not inflate the customer count.


Quick Test

Check what you learned

orders.customer_id is nullable. Which query correctly lists the customers with no orders?

1/5

Day 5

Finished this topic?

Mark it done and your roadmap moves to the next day.

Up nextSubqueries, CTEs & Set Operations
PreviousINNER & LEFT JOIN: Combining Tables

On this page

Summarise with AI

  • ChatGPT
  • Claude
  • Perplexity
  • Grok
  • Google AI Mode
Your next OA is closer than you think.Real questions from this season's OAs
ProductCompany OAsAll ProblemsOA CalendarMock OAsDSA RoadmapSQL RoadmapAgent Engineering
ResourcesInterview experiencesTopicsOA GroupsContributeCompany InsightsOA StorePremiumFor Colleges
OA Helper, powered by NxtWave
TermsPrivacyRefundsTrust & SafetyContact© 2026 OA Helper

Disclaimer: OAHelper is an independent educational platform. We (oahelper.in) do not own the images or questions shown. Content is uploaded by users.