EXISTS vs IN in SQL: Which to Use and When
On this page 8 sections ▾
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;
| name | category | unit_price |
|---|---|---|
| Beacon Analytics | Software | 1450 |
| Aurora Laptop 14" | Hardware | 1299 |
| Atlas Cloud Suite | Software | 899 |
| Vertex Monitor 27" | Hardware | 449.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');
| name | category |
|---|---|
| Nimbus Docking Hub | Accessories |
| Quill Keyboard | Accessories |
| Harbor Support Plan | Services |
● 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
INon some databases, because the list may be materialised. - A correlated
EXISTSwith 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';
| company | status |
|---|---|
| Meridian Trading Co. | Shipped |
| Acme Robotics | Shipped |
| Andes Textiles | Shipped |
| Cedar Health | Shipped |
| Fjord Analytics | Shipped |
● 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:
| Situation | Use |
|---|---|
| Filtering by a small fixed list of values | IN 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 table | JOIN |
| The inner column can be NULL | NOT 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:
- Customers with a shipped orderMedium
- Customers still waitingMedium
- Put a name to the departmentMedium
- Who is closing the deals?Hard
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.
Related reading
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.