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 != NULLTRUE AND TRUE AND UNKNOWNUNKNOWN
  • Строки с 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.