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)Как исправить:
- Добавить
WHERE user_id IS NOT NULLв подзапрос:
WHERE id NOT IN (
SELECT user_id FROM orders WHERE user_id IS NOT NULL
)- Использовать 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 минут в день. Больше вопросов -- в примерах вопросов.