Is EXISTS always faster than IN with a subquery, or is that rule of thumb outdated?
answer
- it is repeated more often than it is verified
- both mean the same semi-join
- one suggests probing, the other suggests building
- the optimizer costs both strategies
- read the plan before you rewrite
basics
~20 sIt is outdated as a blanket rule. For an existence filter both express the same semi-join, and cost-based optimizers commonly produce the same plan for either. The honest answer is that the shape you write is a hint, and the plan decides.
solid answer
~50 s`WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)` and `WHERE c.customer_id IN (SELECT o.customer_id FROM orders o)` ask the same question, and duplicates in the inner result are irrelevant to both — `IN` is membership, not a join. Modern cost-based optimizers commonly recognise both as semi-joins and cost the same set of physical strategies, so "EXISTS beats IN" is folklore inherited from engines that evaluated one row-by-row and materialized the other. What the two spellings do differ in is the shape they suggest: the correlated EXISTS reads as a per-outer-row probe, which suits a small, selective outer side with an index on the child key; the uncorrelated IN reads as "build one structure over the child keys, then probe it", which suits a small child set against a large outer table. Write whichever is clearer, then read the plan — and only then rewrite.
code
sql · 7 lines-- Correlated: suggests "probe the child once per outer row"
SELECT c.customer_id FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
-- Uncorrelated: suggests "build the key set once, then test each customer"
SELECT c.customer_id FROM customers c
WHERE c.customer_id IN (SELECT o.customer_id FROM orders o);go deeper
Know that both forms answer the same existence question and that neither duplicates outer rows. Do not repeat the "EXISTS is always faster" rule as fact.
Explain the two strategies the spellings suggest — per-outer-row probe versus build-the-key-set-once — and say which data shapes favour each. Mention that cost-based optimizers commonly consider both regardless of how you wrote it.
Demonstrate the working method: confirm the child join key is indexed, read the plan rather than trusting folklore, and recognise that a bad row estimate, not the idiom, is usually what made the query slow.
The lasting point is about how teams reason: rules of thumb inherited from engines nobody runs anymore quietly become style rules and get enforced in review. Push people toward reading plans and measuring instead of ranking keywords.
## Where the rule of thumb came from "Use EXISTS, never IN" is one of the most durable pieces of SQL advice, and it was once defensible. On older engines that did not transform subqueries, `IN (subquery)` and `EXISTS (subquery)` could compile into genuinely different strategies — one evaluating the inner query once and stashing the result, the other re-evaluating per outer row — with big and predictable differences. The optimizers that made that advice true have largely been superseded, but the advice outlived them, and it now gets repeated in interviews as if it were a law. ## Why the two are the same question Both spellings express a semi-join: keep an outer row when a matching inner row exists, once, regardless of how many matches there are. ```sql -- correlated SELECT c.customer_id FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id); -- uncorrelated SELECT c.customer_id FROM customers c WHERE c.customer_id IN (SELECT o.customer_id FROM orders o); ``` A point worth being explicit about: `IN` does **not** multiply rows. If the subquery returns customer 7 forty times, customer 7 still appears exactly once in the result, because `IN` tests membership in a set rather than pairing rows. That is precisely what distinguishes both of these from an inner join, and it is why neither needs a `DISTINCT`. ## What actually differs: the shape you hand the planner Since the semantics match, the interesting question is what each form suggests as a strategy. - The **correlated EXISTS** reads as: for each outer row, probe the child for a match and stop. That is cheap when the outer side is already small or heavily filtered and the child's join key is indexed. Its total cost scales with the number of outer rows that reach the predicate. - The **uncorrelated IN** reads as: produce the set of child keys once, then test each outer row against it. That is cheap when the child set is small relative to the outer table, or when the outer side is large enough that a per-row probe would be repeated far too often. A good optimizer considers both strategies for either spelling, and picks by estimated cost. So the practical guidance is: the form you write is a hint about intent, not a performance decision, and the way to settle a performance question is to read the plan for both. ## When the spelling still matters in practice There are real cases where you should pay attention rather than shrug: 1. **The child key is not indexed.** Then the correlated form risks a repeated scan of the child table, which is a much worse failure mode than building one structure over it. Fix the index before you fix the spelling. 2. **The estimates are wrong.** If the planner badly misjudges how many outer rows survive the other predicates, it may choose a per-row probe for millions of rows, or build a large structure to serve three. The idiom did not cause that; the estimate did — but the idiom is often what gets blamed. 3. **The subquery is expensive to produce.** If the inner side is itself a heavy aggregation or a multi-table join, whether it is evaluated once or per row is a real difference, and it is worth confirming which one the plan chose. 4. **Extra correlation conditions.** A correlated EXISTS can carry additional predicates referencing the outer row (`AND o.order_date >= c.signup_date`), which the flat `IN (SELECT single_column ...)` form cannot express at all. That is an expressiveness reason to pick EXISTS, not a speed reason, and it is often the real one. ## How to answer this in an interview The answer that lands is not "EXISTS" and not "they are identical". It is: *they express the same semi-join, most modern cost-based optimizers treat them alike, and the difference that survives is the shape of the plan each suggests — per-row probe versus build-once-and-probe. Which wins depends on which side is selective and what is indexed, so I would check the plan rather than apply a rule.* Then add the practical default: write whichever reads more clearly for the query at hand — usually the correlated `EXISTS` when the condition involves more than one column or references the outer row, and `IN` when it is a plain single-column membership test against a short list of keys. ## What not to say Avoid asserting engine-specific behaviour you have not verified: which versions of which engines flatten which subquery forms is a moving target, and a confident wrong claim about it is worse than the honest "it depends on the plan". Avoid the claim that `IN` returns duplicates — it does not. And avoid extending the comparison to the negated forms without care, because `NOT IN` and `NOT EXISTS` differ in ways that go well beyond performance.
- The subquery returns customer 7 forty times. Does the IN form return customer 7 forty times?No — once. `IN` tests whether the value is a member of the set the subquery produced; duplicates in that set change nothing. This is exactly the property an inner join lacks, and the reason neither `IN` nor `EXISTS` needs a `DISTINCT` while a join does.
- Is there a case where you would write EXISTS for a reason other than performance?Yes, and it is the common one: the correlation condition involves more than one column, or references the outer row beyond simple equality — `AND o.order_date >= c.signup_date`, for example. A single-column `IN (SELECT ...)` cannot express that at all, so EXISTS is chosen for expressiveness.
saying these in an interview costs you the question
- EXISTS is faster than IN, that is just how SQL works
- IN always materializes the whole subquery into a temp table
- IN returns one outer row per matching subquery row
- the two forms can return different rows for the same data
- pick by rule of thumb rather than by reading the plan