EXISTS vs IN в SQL — в чём разница и когда что использовать
Коротко
IN проверяет, входит ли значение в список или результат подзапроса. EXISTS проверяет, возвращает ли подзапрос хотя бы одну строку. На практике оба решают похожие задачи, но ведут себя по-разному при NULL, больших объёмах данных и коррелированных подзапросах.
Что такое IN
IN сравнивает значение с набором. Набор может быть списком констант или результатом подзапроса.
-- Список констант
SELECT *
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered');
-- Подзапрос: найти заказы пользователей из Москвы
SELECT *
FROM orders
WHERE user_id IN (
SELECT user_id
FROM users
WHERE city = 'Москва'
);IN с подзапросом: сначала выполняется подзапрос, потом результат используется для фильтрации основной таблицы. Подзапрос выполняется один раз.
Что такое EXISTS
EXISTS проверяет, существуют ли строки, удовлетворяющие условию. Обычно используется с коррелированным подзапросом — подзапросом, который ссылается на внешнюю таблицу.
-- Найти пользователей, у которых есть хотя бы один заказ
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
);
-- Найти товары, которые ни разу не покупали
SELECT *
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.product_id
);EXISTS возвращает TRUE, как только находит первую подходящую строку — не нужно собирать весь результат подзапроса.
SELECT 1 в EXISTS — условность. Можно написать SELECT * или SELECT 42 — не имеет значения, потому что EXISTS проверяет только наличие строк, а не их содержимое.
Ключевые отличия
| IN | EXISTS | |
|---|---|---|
| Что проверяет | Значение в наборе | Наличие строк |
| Подзапрос | Некоррелированный (обычно) | Коррелированный (обычно) |
| NULL-значения | Ловушка! | Безопасен |
| Когда быстрее | Маленький подзапрос | Большой подзапрос с индексом |
| Читаемость | Проще для списков | Проще для «есть/нет связи» |
| NOT-вариант | NOT IN (опасен с NULL!) | NOT EXISTS (безопасен) |
Ловушка NOT IN с NULL
Это самый частый подвох на собеседованиях. Если подзапрос в NOT IN возвращает хотя бы один NULL, результат будет пустым:
-- Допустим, в таблице orders есть строка с user_id = NULL
-- NOT IN вернёт пустой результат!
SELECT *
FROM users
WHERE user_id NOT IN (
SELECT user_id FROM orders
);
-- Результат: 0 строк (даже если есть пользователи без заказов)
-- NOT EXISTS работает корректно
SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
);
-- Результат: пользователи без заказов (как ожидалось)Почему так? NOT IN с NULL работает через трёхзначную логику SQL:
5 NOT IN (1, 2, NULL)→5 != 1 AND 5 != 2 AND 5 != NULL→TRUE AND TRUE AND UNKNOWN→UNKNOWN- Строки с UNKNOWN отфильтровываются
Это классическая ловушка, которая ломает production-запросы. Если используете NOT IN с подзапросом — убедитесь, что в подзапросе нет NULL, или используйте NOT EXISTS.
Производительность
IN с маленьким подзапросом — обычно быстрее. Подзапрос выполняется один раз, результат хешируется, основная таблица фильтруется по хешу.
EXISTS с индексом — обычно быстрее при большом подзапросе. EXISTS останавливается на первой найденной строке и использует индекс по полю связи.
-- EXISTS эффективен, если есть индекс на orders.user_id
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
);
-- IN может быть эффективнее, если подзапрос маленький
SELECT *
FROM orders
WHERE status IN (
SELECT code FROM active_statuses
);На практике в PostgreSQL и других современных СУБД оптимизатор часто преобразует IN в EXISTS и наоборот, поэтому разница в производительности минимальна. Но с NULL ситуация принципиально разная — оптимизатор тут не поможет.
Когда использовать IN
- Фильтрация по списку констант:
WHERE status IN ('active', 'trial') - Подзапрос возвращает маленький набор уникальных значений
- Нет риска NULL в подзапросе
- Код проще читать через «значение входит в набор»
Когда использовать EXISTS
- Проверка наличия связанных записей: «есть ли у пользователя заказы»
- NOT EXISTS вместо NOT IN — безопасно при NULL
- Подзапрос с дополнительными условиями (дата, статус)
- Большие таблицы с индексами на поля связи
-- EXISTS с дополнительным условием — чисто и читаемо
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
AND o.created_at >= '2026-01-01'
AND o.status = 'paid'
);Типичная ошибка
Использование NOT IN там, где нужен NOT EXISTS, и получение пустого результата из-за NULL:
-- Баг: если хотя бы один manager_id IS NULL, вернётся 0 строк
SELECT *
FROM employees
WHERE employee_id NOT IN (
SELECT manager_id FROM departments
);
-- Фикс вариант 1: фильтрация NULL
SELECT *
FROM employees
WHERE employee_id NOT IN (
SELECT manager_id FROM departments
WHERE manager_id IS NOT NULL
);
-- Фикс вариант 2: NOT EXISTS (рекомендуется)
SELECT *
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM departments d
WHERE d.manager_id = e.employee_id
);Вопросы с собеседований
— Чем EXISTS отличается от IN? — IN сравнивает значение с набором, EXISTS проверяет наличие строк в подзапросе. EXISTS обычно используется с коррелированным подзапросом, IN — с некоррелированным. Главное практическое отличие: NOT IN ломается при NULL в подзапросе, NOT EXISTS — нет.
— Что будет, если в подзапросе NOT IN есть NULL? — NOT IN вернёт пустой результат. Причина — трёхзначная логика SQL: сравнение с NULL даёт UNKNOWN, а NOT IN требует, чтобы все сравнения были TRUE. Решение — NOT EXISTS или фильтрация NULL в подзапросе.
— Когда EXISTS быстрее, чем IN? — Когда подзапрос возвращает много строк и есть индекс на поле связи. EXISTS останавливается на первой найденной строке, а IN собирает весь результат подзапроса. Но на практике оптимизатор PostgreSQL часто преобразует одно в другое.
— Можно ли переписать IN через EXISTS и наоборот?
— Да, семантически эквивалентно при отсутствии NULL: WHERE x IN (SELECT y FROM t) = WHERE EXISTS (SELECT 1 FROM t WHERE t.y = x). Но NOT IN и NOT EXISTS НЕ эквивалентны при наличии NULL.
— Что такое коррелированный подзапрос? — Подзапрос, который ссылается на столбец из внешнего запроса. Концептуально выполняется для каждой строки внешнего запроса. EXISTS обычно используется именно с коррелированными подзапросами.
Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.
FAQ
EXISTS и IN — это одно и то же?
Семантически похожи для задач «найти строки со связью», но реализация разная. IN сравнивает значения, EXISTS проверяет существование. Критическое различие проявляется с NOT IN при наличии NULL — результат будет пустым, а NOT EXISTS отработает корректно.
Что быстрее — EXISTS или IN?
В современных СУБД (PostgreSQL, MySQL 8+) оптимизатор часто сам выбирает оптимальный план. Но как правило: IN быстрее с маленьким подзапросом без дубликатов, EXISTS быстрее с большим подзапросом и индексом на поле связи. Проверяйте через EXPLAIN.
Когда использовать NOT EXISTS вместо LEFT JOIN + IS NULL?
Оба подхода дают одинаковый результат для поиска «строк без связи». NOT EXISTS обычно читаемее для этой задачи. По производительности в PostgreSQL разницы почти нет — оптимизатор строит одинаковый план.
Можно ли использовать IN с большим списком?
Да, но есть практические лимиты. В PostgreSQL список IN работает эффективно до ~10 000 значений. Для бо́льших объёмов лучше использовать JOIN с временной таблицей или CTE.