IN и EXISTS в SQL: как проверять принадлежность и наличие связанных строк
Сравнение IN и EXISTS в SQL для аналитики: фильтрация по списку, проверка событий пользователя, NULL и выбор читаемого решения.
Содержание статьи
Нужно выбрать пользователей, которые купили товар из категории electronics, или заказы, для которых есть возврат. IN выглядит коротко, EXISTS — чуть сложнее, но подчёркивает вопрос «существует ли хотя бы одна строка». Неправильный выбор часто связан не со скоростью, а с NULL, дублями и смыслом связи. Разберём вопрос на одном запросе: Когда использовать IN, а когда EXISTS для проверки связанной записи?
Когда нужен этот приём
Когда использовать IN, а когда EXISTS для проверки связанной записи?
Для списка из нескольких статичных тарифов IN читается естественно. Для проверки события оплаты у пользователя EXISTS показывает намерение и не размножает пользователя при десяти платежах. Если подзапрос IN возвращает NULL, логика NOT IN может дать неожиданный UNKNOWN и убрать все строки.
IN подходит для понятного небольшого списка. EXISTS — для факта связанной строки. Для отрицательной проверки предпочитай NOT EXISTS, если в данных возможен NULL. Скорость подтверждай EXPLAIN ANALYZE на реальном объёме.
Логика оператора
IN сравнивает значение с набором значений. EXISTS проверяет, возвращает ли коррелированный подзапрос хотя бы одну строку и может остановиться после первого совпадения. Современный оптимизатор иногда превращает их в похожий semi-join, поэтому не стоит обещать, что один оператор всегда быстрее.
Определи, сравниваешь ли ты значение со списком или проверяешь связь между сущностями. Если связь one-to-many и нужна только проверка факта, начни с EXISTS. Для NOT EXISTS отдельно протестируй строки с NULL и пустые наборы.
- Зафиксируй одну строку результата.
- Отдели условия по исходным строкам от условий по рассчитанным значениям.
- Назови период, ключи связи и правило для повторов.
Рабочая версия
Названия таблиц здесь условные. При переносе сохрани зерно и бизнес-условия, а не буквальный синтаксис примера.
SELECT u.user_id
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id
AND o.status = 'paid'
AND o.paid_at >= DATE '2026-07-01'
);Как сверить ответ
Собери пять-десять строк, где ответ можно получить вручную. Добавь NULL, дубль, объект без связанной записи и значение на границе периода. Затем сравни число строк и уникальных ключей до и после спорного шага.
Выполненный без ошибки запрос ещё не является правильным. Другой аналитик должен суметь восстановить из него источник, фильтр, grain и ожидаемое поведение на крайних случаях.
| Вопрос | Оператор | Комментарий |
|---|---|---|
| значение входит в список | IN | для короткого набора |
| связь существует | EXISTS | не размножает строки |
| связи нет | NOT EXISTS | устойчивее к NULL |
| нужны поля второй таблицы | JOIN | добавляет атрибуты |
Когда выбрать другой способ
NOT IN с NULL — классическая ловушка трёхзначной логики. IN (subquery) не означает JOIN и не даёт колонки второй таблицы. EXISTS не возвращает поля подзапроса. Не делай SELECT DISTINCT после JOIN только для имитации EXISTS.
IN подходит для понятного небольшого списка. EXISTS — для факта связанной строки. Для отрицательной проверки предпочитай NOT EXISTS, если в данных возможен NULL. Скорость подтверждай EXPLAIN ANALYZE на реальном объёме.
Десять платежей одного пользователя не создают десять строк результата.
Небольшой эксперимент
Напиши два отчёта: пользователи с оплатой и пользователи без оплаты. Сгенерируй случай с двумя оплатами и NULL в справочнике. Сравни IN, EXISTS, NOT IN и NOT EXISTS и объясни результат словами, а не только ссылкой на план.
- Сначала запиши ожидаемый результат словами.
- Проверь запрос на маленькой контрольной выборке.
- Объясни, что запрос считает и чего не доказывает.
Следующий шаг
IN подходит для понятного небольшого списка. EXISTS — для факта связанной строки. Для отрицательной проверки предпочитай NOT EXISTS, если в данных возможен NULL. Скорость подтверждай EXPLAIN ANALYZE на реальном объёме.
Материалы по теме

Подзапросы в SQL: когда нужен вложенный SELECT, CTE или JOIN
Как выбрать между подзапросом, CTE и JOIN: где меняется зерно данных, почему скалярный подзапрос дорого стоит и как разложить длинный запрос на проверяемые слои.

Коррелированный подзапрос: как он выполняется и чем его заменить
Почему коррелированный подзапрос выполняется для каждой строки, когда он оправдан, как заменить его оконной функцией или предагрегацией и не изменить при этом смысл расчёта.

SQL CTE: как разделить сложный запрос на понятные шаги
Разбираем WITH и CTE на задачах аналитика: как сначала собрать активных пользователей, затем присоединить сегменты и проверить каждый этап расчёта.