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.