pbPassingBI
/
Subqueries & CTEs intermediate 5 min

EXISTS versus IN

Checking for existence, and why NOT EXISTS is the safe default.

What you'll be able to do
  • Choose between IN, EXISTS and JOIN
  • Explain why NOT IN fails on nulls
  • Write an anti-join

IN

SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);

Reads well, and is fine for a small literal list or a subquery that cannot return nulls.

EXISTS

SELECT name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);

EXISTS stops at the first match, so it does not care how many rows the inner query could return. SELECT 1 is conventional — the column list is never evaluated.

Why NOT EXISTS beats NOT IN

-- returns ZERO rows if any customer_id in orders is NULL
WHERE customer_id NOT IN (SELECT customer_id FROM orders)

-- correct regardless of nulls
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)

With NOT IN, a single null makes every comparison unknown, so nothing qualifies — and there is no error to tell you. Default to NOT EXISTS.

Versus a join

A join is right when you need columns from the other table. But a join can multiply rows, and EXISTS never does — so for a pure does this exist question, EXISTS is both safer and clearer.

The LEFT JOIN anti-join pattern is the third option:

LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.customer_id IS NULL

Often the fastest on large sets, and null-safe.

Key points
  • NOT IN returns nothing if the subquery yields a NULL — use NOT EXISTS
  • EXISTS short-circuits on the first match and never multiplies rows
  • Use a join when you need columns from the other table
Check yourself