All three can express "give me rows from A that have a match in B," but they don't always perform identically, particularly at scale:
-- EXISTS: stops at the first match per outer row, generally handles NULLs safely
SELECT * FROM Employees e
WHERE EXISTS (SELECT 1 FROM Departments d WHERE d.dept = e.dept);
-- IN: needs the full subquery result materialized (or at least conceptually available) up front
SELECT * FROM Employees e
WHERE e.dept IN (SELECT dept FROM Departments);
Modern optimizers frequently rewrite these into equivalent plans when the two are logically the same, so the historical advice "EXISTS is always faster" is less universally true than it once was — but EXISTS remains the safer default for two concrete reasons: it isn't vulnerable to the NOT IN / NULL trap covered in SQL Fundamentals: Joins, NULL, Aggregate Functions, and Subqueries, and it doesn't require the subquery's full result set to be built before the outer query can proceed on engines that don't optimize the two identically. For large subqueries, EXISTS (or an equivalent JOIN) is the generally recommended default; IN is perfectly fine for small, known, NULL-free lists where readability favors it.