RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
SQL questions
TrickySQL#1047 min readJul 11, 2026

EXISTS vs IN

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because one hidden NULL can make a query look correct and still return the wrong rows.

Question: What is the difference between EXISTS and IN in SQL?

Answer: EXISTS checks whether a subquery returns at least one row, while IN checks whether a value matches one of the returned values. In many databases they can produce the same result, but they behave differently around NULL values and the optimizer may execute them differently. A safe rule is: use EXISTS for existence checks, and be careful with NOT IN because a single NULL can break it.

Interview-Ready Answer: I use EXISTS when I want to know whether related rows exist, and I use IN when I want to compare a value against a list or subquery result. The big gotcha is NULL: NOT IN can return no rows if the subquery contains even one NULL, so in practice I often prefer NOT EXISTS for exclusion logic. Also, modern optimizers often turn both into a semi-join, so the best choice is usually the one that is clearest and safest for the data.

🧠 Memory Map
Memory map — visual summary of this topic

What they mean

Detailed Explanation: Think of EXISTS as a yes/no test: “Does at least one matching row exist?” Think of IN as a membership test: “Is this value inside this set?” Both are often used in subqueries, but they answer the question in slightly different ways.

How the database thinks about it

  1. The outer query picks a row, such as one customer.
  2. For EXISTS, the database runs the subquery with that outer row’s values plugged in if the subquery is correlated, meaning it refers back to the outer query.
  3. As soon as one matching row is found, EXISTS is satisfied. It does not need the rest of the rows, so the engine can stop early.
  4. For IN, the database checks whether the compared value equals one of the values produced by the subquery.
  5. If the subquery has no NULL, this is straightforward. If the subquery can return NULL, SQL’s three-valued logic kicks in: TRUE, FALSE, or UNKNOWN. That is why NOT IN is dangerous.
  6. Modern optimizers often rewrite both forms into a semi-join, which is a join used only to test existence, not to return matching inner rows. For exclusion, they may use an anti-join.

EXISTS vs IN vs JOIN

ConceptBest forMain risk
EXISTSTesting row existenceCan be less obvious if overused
INComparing to a known setNULL traps in subqueries
JOINReturning columns from both sidesDuplicate outer rows

When to use each

  • Use EXISTS for “does a related row exist?” questions, especially with correlated subqueries.
  • Use IN for small, clean lists or when the subquery is guaranteed not to return NULL.
  • Use NOT EXISTS instead of NOT IN when excluding rows from another table.
  • Use a JOIN when you need data from the matched table, not just a yes/no answer.

Performance and complexity

In theory, a naive membership test can be expensive if the subquery is scanned again and again. In practice, optimizers usually do much better: they may build a hash set, use an index lookup, or rewrite the query into a semi-join. A good mental model is EXISTS can stop at the first match, while IN may need a complete value set before filtering, but the actual plan depends on the database, indexes, and statistics. On large tables, a well-indexed correlated EXISTS might probe an index millions of times efficiently; without indexes, both forms can degrade toward nested-loop behavior and become slow. Realistically, if your outer table has 5 million rows and the inner table has 200,000 rows, the difference between an indexed semi-join and a full scan can be seconds versus minutes.

Important edge cases

  • NOT IN with a NULL in the subquery can eliminate every row, which surprises many candidates.
  • EXISTS ignores the columns selected inside it, so SELECT 1 and SELECT * are logically equivalent there.
  • IN with duplicates does not change the truth value, but duplicates can still affect how much work the engine does before optimization.
  • If the subquery is uncorrelated, IN can be very natural: it behaves like membership in a derived set.

Memory-wise, remember this: EXISTS asks “is there at least one seat?” while IN asks “is my ticket number on the guest list?”

Real-World Example: Imagine a checkout service for an e-commerce app that decides whether a customer can use a loyalty coupon. The query checks whether the customer has any completed orders, and a developer writes it with NOT IN against an orders table. One dirty row has a NULL customer id from a bad import, and suddenly the exclusion query returns nothing at all, so every customer looks ineligible. The symptom is brutal: support tickets spike, logs show the coupon-eligibility query returning zero rows, and the UI quietly hides discounts from everyone during a promotion.

The fix is usually to switch to NOT EXISTS and add data-cleanup or a NOT NULL constraint where appropriate. That way the business logic stays correct even if one bad row sneaks into the table.

SQL
-- Demonstration of EXISTS vs IN, including the classic NULL edge case.
-- This script is self-contained and can be run in a SQL database that supports
-- standard CTEs and VALUES constructors.

-- Customers we want to test
WITH customers(id, name) AS (
    VALUES
        (1, 'Ada'),
        (2, 'Ben'),
        (3, 'Cleo'),
        (4, 'Drew')
),
-- Orders includes a NULL customer_id to show the NOT IN trap
orders(order_id, customer_id) AS (
    VALUES
        (101, 1),
        (102, 1),
        (103, 2),
        (104, NULL)
)
SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
)
ORDER BY c.id;

-- Same business result using IN: customers who have at least one order.
WITH customers(id, name) AS (
    VALUES
        (1, 'Ada'),
        (2, 'Ben'),
        (3, 'Cleo'),
        (4, 'Drew')
),
orders(order_id, customer_id) AS (
    VALUES
        (101, 1),
        (102, 1),
        (103, 2),
        (104, NULL)
)
SELECT c.id, c.name
FROM customers c
WHERE c.id IN (
    SELECT o.customer_id
    FROM orders o
)
ORDER BY c.id;

-- Failure path: NOT IN with a NULL in the subquery produces no rows.
-- This happens because SQL cannot prove that c.id is not equal to NULL.
WITH customers(id, name) AS (
    VALUES
        (1, 'Ada'),
        (2, 'Ben'),
        (3, 'Cleo'),
        (4, 'Drew')
),
orders(order_id, customer_id) AS (
    VALUES
        (101, 1),
        (102, 1),
        (103, 2),
        (104, NULL)
)
SELECT c.id, c.name
FROM customers c
WHERE c.id NOT IN (
    SELECT o.customer_id
    FROM orders o
)
ORDER BY c.id;

-- Safe alternative: NOT EXISTS ignores unrelated NULLs and matches by the join condition.
WITH customers(id, name) AS (
    VALUES
        (1, 'Ada'),
        (2, 'Ben'),
        (3, 'Cleo'),
        (4, 'Drew')
),
orders(order_id, customer_id) AS (
    VALUES
        (101, 1),
        (102, 1),
        (103, 2),
        (104, NULL)
)
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
)
ORDER BY c.id;

Follow-up & Tricky Questions:

  • When would you choose EXISTS over a JOIN?
    Use EXISTS when you only need to know whether a related row is present. Use a JOIN when you actually need columns from the other table or want to aggregate across matched rows.
  • Is EXISTS always faster than IN?
    No. Modern optimizers often rewrite both into similar plans, so performance depends on indexes, row counts, statistics, and the exact database engine.
  • Why is NOT IN risky?
    Because a single NULL in the subquery can make the predicate evaluate to UNKNOWN for every row, which means you may get no results at all.
  • Does SELECT 1 inside EXISTS matter?
    Not for the logic. The database only checks whether a row exists, so the selected columns are ignored; people use SELECT 1 as a clear convention.
  • What if the subquery returns duplicates?
    Duplicates do not change the truth value of EXISTS or IN, but they can affect work done before optimization. The optimizer may remove or ignore them depending on the plan.

Tricky gotchas:

  • Is WHERE x IN (subquery) the same as many OR conditions?
    Logically yes for a fixed list of values, but not always in execution behavior. The engine can optimize IN into a set lookup, which is usually cleaner and faster than a long chain of ORs.
  • Does EXISTS return the matching inner row?
    No. It returns only true or false for the outer row; if you need the matched data, use a JOIN or a subquery that selects the columns explicitly.
  • Is NOT EXISTS the exact opposite of EXISTS?
    Logically yes, and it is the safer anti-match pattern. It avoids the NULL trap that makes NOT IN so error-prone.

Common Mistakes:

  • Using NOT IN on a nullable subquery. Correction: prefer NOT EXISTS unless you are sure the subquery cannot return NULL.
  • Thinking EXISTS needs SELECT *. Correction: the selected columns do not matter; SELECT 1 is enough and clearer.
  • Using JOIN when only existence is needed. Correction: a JOIN can duplicate rows, while EXISTS naturally answers the yes/no question.
  • Assuming performance based only on syntax. Correction: always think about the execution plan, indexes, and whether the optimizer can rewrite the query.

Memory Hook: EXISTS = “Is anyone there?” IN = “Is my name on the list?” If you remember that, the NULL trap becomes easier to spot.

Cheat Sheet:

  • EXISTS checks for at least one matching row.
  • IN checks membership in a set of values.
  • NOT EXISTS is usually safer than NOT IN.
  • NULL can make NOT IN behave unexpectedly.
  • Optimizers often turn both into semi-joins or anti-joins.
  • Use the clearest form that matches your intent.

Practice Tasks:

  • Write a query that finds customers with at least one paid order using EXISTS.
  • Rewrite the same query using IN and compare the result.
  • Add a NULL into the subquery and observe why NOT IN breaks, then fix it with NOT EXISTS.
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

-- Demonstration of EXISTS vs IN, including the classic NULL edge case. -- This script is self-contained and can be run in a SQL database that supports -- standard CTEs and VALUES constructors. -- Customers we want to test WITH customers(id, name) AS ( VALUES (1, 'Ada'), (2, 'Ben'), (3, 'Cleo'), (4, 'Drew') ), -- Orders includes a NULL customer_id to show the NOT IN trap orders(order_id, customer_id) AS ( VALUES (101, 1), (102, 1), (103, 2), (104, NULL) ) SELECT c.id, c.name FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) ORDER BY c.id; -- Same business result using IN: customers who have at least one order. WITH customers(id, name) AS ( VALUES (1, 'Ada'), (2, 'Ben'), (3, 'Cleo'), (4, 'Drew') ), orders(order_id, customer_id) AS ( VALUES (101, 1), (102, 1), (103, 2), (104, NULL) ) SELECT c.id, c.name FROM customers c WHERE c.id IN ( SELECT o.customer_id FROM orders o ) ORDER BY c.id; -- Failure path: NOT IN with a NULL in the subquery produces no rows. -- This happens because SQL cannot prove that c.id is not equal to NULL. WITH customers(id, name) AS ( VALUES (1, 'Ada'), (2, 'Ben'), (3, 'Cleo'), (4, 'Drew') ), orders(order_id, customer_id) AS ( VALUES (101, 1), (102, 1), (103, 2), (104, NULL) ) SELECT c.id, c.name FROM customers c WHERE c.id NOT IN ( SELECT o.customer_id FROM orders o ) ORDER BY c.id; -- Safe alternative: NOT EXISTS ignores unrelated NULLs and matches by the join condition. WITH customers(id, name) AS ( VALUES (1, 'Ada'), (2, 'Ben'), (3, 'Cleo'), (4, 'Drew') ), orders(order_id, customer_id) AS ( VALUES (101, 1), (102, 1), (103, 2), (104, NULL) ) SELECT c.id, c.name FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id ) ORDER BY c.id;