Intermediate

EXISTS vs IN in SQL: Which to Use and When

7 min readUpdated September 10, 2026Every example verified
On this page 8 sections ▾
  1. The same question, two ways
  2. The one that actually matters: NOT IN and NULL
  3. When you cannot use the other one
  4. What about performance?
  5. Or use a JOIN instead
  6. Practice this topic
  7. Frequently asked questions
  8. Related reading

Both answer the question “does a matching row exist?”, and most of the time both give the same answer. They are not interchangeable, though, and the difference shows up in exactly two places: what happens when NULLs are involved, and what you can put in the subquery.

This page shows both forms side by side, then the one case where choosing wrong produces a silently empty result.

The same question, two ways

Which customers have had an order shipped? With IN, the subquery returns a list of ids and the outer query checks membership:

SELECT company
FROM Customers
WHERE customer_id IN (
    SELECT customer_id
    FROM Orders
    WHERE status = 'Shipped'
);
company
Meridian Trading Co.
Acme Robotics
Andes Textiles
Cedar Health
Fjord Analytics

5 rows · produced by running this query on the sample database

With EXISTS, the subquery is a yes/no test that refers back to the current row:

SELECT company
FROM Customers c
WHERE EXISTS (
    SELECT 1
    FROM Orders o
    WHERE o.customer_id = c.customer_id
      AND o.status = 'Shipped'
);
company
Meridian Trading Co.
Acme Robotics
Andes Textiles
Cedar Health
Fjord Analytics

5 rows · produced by running this query on the sample database

Identical results. The difference in shape is that IN compares a value against a list, while EXISTS asks whether a correlated subquery returns any row at all — it stops at the first one it finds and never looks at what that row contains.

SELECT 1 versus SELECT *

Inside EXISTS the select list is irrelevant, because only the existence of a row matters. SELECT 1 makes that explicit to whoever reads the query next; every serious database optimiser treats it identically to SELECT *, so this is a readability choice, not a performance one.

The one that actually matters: NOT IN and NULL

The positive forms behave the same. The negative forms do not, and this is the single most useful thing on this page.

x NOT IN (1, 2, NULL) expands to x <> 1 AND x <> 2 AND x <> NULL. That last comparison can never be true — nothing is known to be unequal to an unknown value — so the whole AND chain is unknown, and the row is not returned. One NULL anywhere in the list and the query returns nothing at all. No error, no warning, just an empty result.

SELECT company
FROM Customers
WHERE customer_id NOT IN (
    SELECT customer_id
    FROM Orders
    WHERE status = 'Shipped'
);
company
Sakura Logistics
Kilimanjaro Foods
Baltic Systems

3 rows · produced by running this query on the sample database

That query is safe only because Orders.customer_id happens to have no NULLs. Change the subquery to a nullable column and it becomes a trap.

NOT EXISTS has no such problem, because it never compares anything — it only asks whether a row came back:

SELECT company
FROM Customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM Orders o
    WHERE o.customer_id = c.customer_id
      AND o.status = 'Shipped'
);
company
Sakura Logistics
Kilimanjaro Foods
Baltic Systems

3 rows · produced by running this query on the sample database

An honesty note about this playground

On the engine this site runs in your browser, NOT IN with a NULL in the list does not reproduce the trap — it still returns rows. On PostgreSQL, MySQL, SQL Server, Oracle and SQLite it does, which is what you will meet at work. The rule stands: when the inner column is nullable, write NOT EXISTS, or filter the NULLs out inside the subquery with WHERE column IS NOT NULL.

When you cannot use the other one

  • IN can take a literal list. WHERE category IN ('Hardware', 'Software') needs no subquery at all, and EXISTS has no equivalent for that.
  • EXISTS can match on several columns. A correlated subquery can compare as many columns as it likes. Standard SQL allows row constructors with IN, but support is patchy and the syntax is awkward, so EXISTS is the practical choice.
  • IN reads better for a small fixed set. Nobody should write an EXISTS to check three known values.
  • EXISTS reads better when the subquery is a condition. If the inner query exists purely to answer yes or no, EXISTS says that out loud.

The literal-list form, which is where most people meet IN first:

SELECT name, category, unit_price
FROM Products
WHERE category IN ('Hardware', 'Software')
ORDER BY unit_price DESC;
namecategoryunit_price
Beacon AnalyticsSoftware1450
Aurora Laptop 14"Hardware1299
Atlas Cloud SuiteSoftware899
Vertex Monitor 27"Hardware449.99

4 rows · produced by running this query on the sample database

And its negation, which is safe here because the list has no NULL in it:

SELECT name, category
FROM Products
WHERE category NOT IN ('Hardware', 'Software');
namecategory
Nimbus Docking HubAccessories
Quill KeyboardAccessories
Harbor Support PlanServices

3 rows · produced by running this query on the sample database

What about performance?

The honest answer for any database written in the last fifteen years: usually nothing to choose between them. Query planners recognise both shapes and turn them into the same semi-join internally, so the old advice that “EXISTS is faster because it stops early” is mostly folklore now.

What does still matter:

  • A subquery returning a very large list can be worse with IN on some databases, because the list may be materialised.
  • A correlated EXISTS with no index on the joining column is slow for the ordinary reason — it is doing a lookup per outer row. Index the column and it is fast. See how indexes work.
  • Choose for correctness and readability first. If a query is genuinely slow, read the plan rather than guessing at the operator.

Or use a JOIN instead

If you need columns from the other table in your output, neither operator is right — use a join:

SELECT DISTINCT c.company, o.status
FROM Customers c
JOIN Orders o ON o.customer_id = c.customer_id
WHERE o.status = 'Shipped';
companystatus
Meridian Trading Co.Shipped
Acme RoboticsShipped
Andes TextilesShipped
Cedar HealthShipped
Fjord AnalyticsShipped

5 rows · produced by running this query on the sample database

Note the DISTINCT, which is the price of the join: a customer with three shipped orders appears three times. That is the rule of thumb for choosing:

Which one to reach for
SituationUse
Filtering by a small fixed list of valuesIN with a literal list
Filtering on "a matching row exists"EXISTS
Filtering on "no matching row exists"NOT EXISTS
You need columns from the other tableJOIN
The inner column can be NULLNOT EXISTS - never NOT IN

Practice this topic

Reading explains the idea; producing it yourself is what makes it stick. These exercises use exactly what this page covers, and your answer is checked by running it against the same sample database:

See all 42 exercises →

Frequently asked questions

What is the difference between EXISTS and IN in SQL?

IN compares a value against a list of results from a subquery or a literal list. EXISTS asks whether a correlated subquery returns any row at all and stops at the first match, ignoring what the row contains. For positive checks they usually return the same rows; the difference matters for their negations and when NULLs are involved.

Why does NOT IN return no rows?

Because the subquery produced a NULL. NOT IN expands to a chain of not-equal comparisons, and comparing anything to NULL is unknown rather than true, which makes the whole condition unknown and returns no rows. Use NOT EXISTS, or add WHERE column IS NOT NULL inside the subquery.

Is EXISTS faster than IN?

On modern databases, usually not - the planner rewrites both into the same semi-join. The advice that EXISTS short-circuits is largely historical. Choose for correctness and clarity, and if a query is slow, read its execution plan instead of swapping operators.

Why is NOT EXISTS safer than NOT IN?

NOT EXISTS never compares values - it only reports whether the subquery returned a row - so a NULL in the inner result cannot make the condition unknown. NOT IN compares, and a single NULL makes the entire condition unknown, silently returning zero rows.

Should I use SELECT 1 or SELECT * inside EXISTS?

Either; the select list is ignored because only the existence of a row matters. SELECT 1 makes that intent obvious to the next reader, which is the only real reason to prefer it.

When should I use a JOIN instead of IN or EXISTS?

Whenever you need columns from the other table in your output. IN and EXISTS are filters and return nothing from the inner table. A join gives you the columns but can multiply rows when the match is one-to-many, which is why joined filters often need DISTINCT or an aggregate.

About the author

SQL Practice

SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.