IN в SQL — фильтрация по списку значений

Коротко

IN -- оператор фильтрации в SQL, который проверяет, входит ли значение в заданный набор. Вместо цепочки из пяти OR вы пишете один IN -- запрос становится короче и читаемее. IN работает со списком констант и с подзапросами, а его инверсия NOT IN -- один из главных источников ошибок на собеседованиях из-за поведения с NULL.

Синтаксис IN

SELECT column1, column2
FROM table_name
WHERE column IN (value1, value2, value3)

Если значение столбца совпадает хотя бы с одним элементом списка, строка попадает в результат. IN работает в WHERE и HAVING.

IN со списком значений

Самый частый случай -- фильтрация по нескольким конкретным значениям:

SELECT user_id, city, status
FROM users
WHERE city IN ('Москва', 'Санкт-Петербург', 'Казань')

Эквивалентно:

WHERE city = 'Москва' OR city = 'Санкт-Петербург' OR city = 'Казань'

IN короче, проще для чтения и менее подвержен ошибкам с приоритетом AND/OR. Если в WHERE есть ещё условия, IN безопаснее -- не нужны скобки вокруг OR:

-- С IN -- чисто и понятно
WHERE city IN ('Москва', 'Казань') AND status = 'active'

-- С OR -- нужны скобки, иначе логика сломается
WHERE (city = 'Москва' OR city = 'Казань') AND status = 'active'

IN с числами, строками, датами

IN работает с любыми типами данных:

-- Числа
WHERE department_id IN (1, 3, 7)

-- Строки
WHERE status IN ('paid', 'shipped', 'delivered')

-- Даты
WHERE order_date IN ('2026-01-01', '2026-02-01', '2026-03-01')

Для дат IN удобен, когда нужны конкретные даты. Для диапазонов лучше подойдёт BETWEEN или >= / <.

IN с подзапросом

IN принимает не только список констант, но и результат SELECT-запроса:

SELECT name, email
FROM users
WHERE id IN (
    SELECT DISTINCT user_id
    FROM orders
    WHERE amount > 10000
)

Подзапрос возвращает список user_id покупателей с крупными заказами. Внешний запрос фильтрует пользователей по этому списку. Подробнее о видах подзапросов -- в гайде по подзапросам.

NOT IN

NOT IN -- инверсия: строка попадает в результат, если значение не входит в набор.

SELECT user_id, name
FROM users
WHERE user_id NOT IN (
    SELECT DISTINCT user_id
    FROM orders
    WHERE created_at >= '2026-01-01'
)

Этот запрос находит пользователей без заказов с начала года. Выглядит просто, но есть ловушка.

NOT IN и NULL -- главная ловушка

Если в списке или подзапросе есть хотя бы один NULL, NOT IN вернёт пустой результат. Это самая частая ошибка, связанная с IN.

Почему так происходит: NOT IN (1, 2, NULL) разворачивается в x <> 1 AND x <> 2 AND x <> NULL. Сравнение с NULL даёт UNKNOWN, AND с UNKNOWN даёт UNKNOWN, и ни одна строка не проходит фильтр.

-- Если в orders.user_id есть NULL -- результат будет пустым!
SELECT name
FROM users
WHERE id NOT IN (SELECT user_id FROM orders)

Как исправить:

  1. Добавить WHERE user_id IS NOT NULL в подзапрос:
WHERE id NOT IN (
    SELECT user_id FROM orders WHERE user_id IS NOT NULL
)
  1. Использовать NOT EXISTS -- он корректно работает с NULL:
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = users.id
)

NOT EXISTS -- безопаснее. Подробное сравнение -- в статье EXISTS vs IN.

Производительность IN

Небольшой список констант (до 100 элементов) -- IN работает быстро, оптимизатор использует индекс по столбцу.

Подзапрос -- оптимизатор PostgreSQL и MySQL обычно преобразует IN (SELECT ...) в полусоединение (semi-join), что по производительности близко к EXISTS. На практике разница минимальна.

Большие списки (тысячи значений) -- IN с длинным списком констант может замедлить разбор запроса и увеличить план выполнения. Если список растёт -- вынесите значения во временную таблицу или CTE и используйте JOIN.

-- Плохо: IN с 5000 значений
WHERE user_id IN (1, 2, 3, ..., 5000)

-- Лучше: загрузить во временную таблицу и JOIN
WITH target_users AS (
    SELECT unnest(ARRAY[1, 2, 3, ...]) AS user_id
)
SELECT u.*
FROM users u
JOIN target_users t ON t.user_id = u.id

Практические примеры

Заказы в определённых статусах:

SELECT order_id, user_id, status, amount
FROM orders
WHERE status IN ('pending', 'processing', 'shipped')
ORDER BY created_at DESC

Пользователи, купившие конкретные товары:

SELECT DISTINCT u.name, u.email
FROM users u
JOIN order_items oi ON oi.user_id = u.id
WHERE oi.product_id IN (
    SELECT id FROM products WHERE category = 'premium'
)

Активные пользователи из целевых городов:

SELECT user_id, city, last_active_at
FROM users
WHERE city IN ('Москва', 'Санкт-Петербург', 'Новосибирск')
  AND last_active_at >= CURRENT_DATE - INTERVAL '7 days'
  AND status NOT IN ('banned', 'deleted')

Типичные ошибки

NOT IN с NULL. WHERE id NOT IN (SELECT user_id FROM orders) -- если в user_id есть NULL, результат пустой. Используйте NOT EXISTS или фильтруйте NULL в подзапросе.

IN вместо BETWEEN для диапазонов. WHERE age IN (18, 19, 20, ..., 65) -- технически работает, но BETWEEN короче и эффективнее: WHERE age BETWEEN 18 AND 65.

Забытые скобки с OR вместо IN. WHERE status = 'active' OR status = 'trial' AND plan = 'premium' -- AND свяжется с trial, а не с обоими статусами. IN решает эту проблему: WHERE status IN ('active', 'trial') AND plan = 'premium'.

Дубликаты в подзапросе. IN корректно работает с дубликатами (не удваивает строки), но добавление DISTINCT в подзапрос может ускорить выполнение, если подзапрос возвращает миллионы строк.

Вопросы с собеседований

-- Чем IN отличается от EXISTS? -- IN сравнивает значение со списком. EXISTS проверяет, вернул ли подзапрос хотя бы одну строку. NOT IN ломается при NULL в подзапросе, NOT EXISTS -- нет. На больших таблицах EXISTS может быть быстрее за счёт раннего выхода. Подробнее: EXISTS vs IN.

-- Почему NOT IN с NULL-значениями в подзапросе возвращает пустой результат? -- Потому что NOT IN разворачивается в цепочку <>, а x <> NULL даёт UNKNOWN. AND с UNKNOWN тоже даёт UNKNOWN, и ни одна строка не проходит фильтр. Решение: NOT EXISTS или WHERE column IS NOT NULL в подзапросе.

-- IN или цепочка OR -- есть ли разница в производительности? -- Нет. Оптимизатор преобразует IN в цепочку OR и наоборот. Разница только в читаемости -- IN компактнее.

-- Напишите запрос: найдите пользователей из Москвы или Питера, которые делали заказы на сумму более 5000. -- SELECT DISTINCT u.name FROM users u JOIN orders o ON o.user_id = u.id WHERE u.city IN ('Москва', 'Санкт-Петербург') AND o.amount > 5000.

-- Можно ли использовать IN в HAVING? -- Да. HAVING COUNT(*) IN (1, 2, 3) -- корректный синтаксис, хотя на практике для агрегатов чаще используют BETWEEN или операторы сравнения.

Потренируйтесь

IN -- базовый оператор, но ловушки с NULL и подзапросами ловят даже опытных аналитиков. Потренируйтесь на реальных задачах -- откройте тренажёр с 1500+ вопросами по SQL и разборами.

FAQ

Сколько значений можно передать в IN?

Формально ограничение зависит от СУБД. PostgreSQL допускает тысячи элементов, но запросы с сотнями значений в IN становятся тяжёлыми для парсера. Если список больше 100 элементов, лучше загрузить значения во временную таблицу и использовать JOIN.

IN работает с NULL-значениями в столбце?

IN корректно обрабатывает NULL в проверяемом столбце: строка с NULL просто не попадёт в результат, потому что NULL = 'value' даёт UNKNOWN. Проблема возникает только с NOT IN, когда NULL присутствует в самом списке.

Можно ли комбинировать IN с другими операторами в WHERE?

Да. IN -- обычное условие, его можно комбинировать через AND и OR с любыми другими условиями: WHERE city IN ('Москва', 'Казань') AND status = 'active' AND created_at >= '2026-01-01'.

Как тренироваться

IN встречается в каждом втором SQL-запросе на собеседовании, особенно в связке с подзапросами и NOT IN. В тренажёре Карьерник есть задачи на фильтрацию с разборами -- тренируйтесь по 15 минут в день. Больше вопросов -- в примерах вопросов.