IN сам по себе — это «проверка членства»: значение должно быть в списке. У него две формы.
С литералами — список значений прямо в запросе:
WHERE country IN ('RU', 'BY', 'KZ')
С подзапросом — список вычисляется другим SELECT:
WHERE customer_id IN (SELECT id FROM vip_customers)
Вторая форма — гибче и часто нужна в реальных задачах. Эту статью посвятим именно ей.
Зачем нужен IN с подзапросом
Когда список «членов» неизвестен заранее или вычисляется. Сценарии:
- Заказы клиентов из определённого сегмента (VIP, активные, новые).
- Комментарии к постам с определённым тегом.
- Пользователи, у которых есть хотя бы один платный заказ.
Без подзапроса пришлось бы тащить список в код приложения, форматировать его и подставлять — медленнее и грязнее.
Базовый синтаксис
SELECT *
FROM таблица_A
WHERE column IN (
SELECT column_b FROM таблица_B WHERE условие
);
Подзапрос возвращает список значений (одна колонка). Внешний запрос проверяет, есть ли значение column в этом списке.
Пример с таблицами
customers:
| id |
name |
tier |
| 1 |
Аня |
gold |
| 2 |
Боб |
free |
| 3 |
Вера |
gold |
| 4 |
Гриша |
free |
orders:
| id |
customer_id |
amount |
| 1 |
1 |
100 |
| 2 |
2 |
50 |
| 3 |
1 |
200 |
| 4 |
3 |
300 |
Запрос «заказы только VIP-клиентов (tier = gold)»:
SELECT *
FROM orders
WHERE customer_id IN (
SELECT id FROM customers WHERE tier = 'gold'
);
Подзапрос вернёт (1, 3) — id-шники Ани и Веры. Внешний запрос оставит только заказы, где customer_id есть в этом списке:
| id |
customer_id |
amount |
| 1 |
1 |
100 |
| 3 |
1 |
200 |
| 4 |
3 |
300 |
Заказ Боба (#2) отфильтровался — он free, не VIP.
NOT IN — обратная проверка
SELECT *
FROM orders
WHERE customer_id NOT IN (
SELECT id FROM customers WHERE tier = 'gold'
);
Логически: «такие, где значения нет в списке». Но тут начинается боль с NULL.
Главная ловушка: NOT IN с NULL
NOT IN ломается на NULL в подзапросе. Это не баг Postgres — это сама логика SQL про NULL:
WHERE id NOT IN (SELECT customer_id FROM orders);
Логика SQL: id NOT IN (1, 3, NULL) — это id <> 1 AND id <> 3 AND id <> NULL. Последняя часть всегда UNKNOWN (= не TRUE), и весь AND тоже UNKNOWN. Итог — ничего не выбирается, даже если у id нет совпадений с 1 и 3.
Это ловит почти всех новичков: запрос выглядит правильно, тестовые данные не содержат NULL, всё работает на dev. На проде в одной строке появляется NULL — и весь отчёт молча возвращает 0 строк.
Лекарство: всегда фильтровать NULL в подзапросе или использовать NOT EXISTS:
WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL);
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = u.id);
Правило: для anti-join всегда NOT EXISTS, забудь про NOT IN.
IN vs EXISTS — когда что
WHERE c.id IN (SELECT customer_id FROM orders);
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
Оба работают. На современных Postgres оптимизатор обычно превращает IN (subquery) в семантику EXISTS под капотом — производительность похожа.
| Критерий |
IN (subquery) |
EXISTS |
| Простота |
Чуть короче для одной колонки |
Чуть длиннее |
| Безопасность с NULL |
NOT IN ломается на NULL |
NOT EXISTS — безопасен |
| Несколько колонок |
(a,b) IN (SELECT a,b FROM ...) — некрасиво |
Естественно через AND |
| Условия на внешнюю таблицу |
Только через сравнение |
Любое условие в подзапросе |
Простое правило: для IN — пиши IN. Для NOT IN — пиши NOT EXISTS.
IN со списком литералов vs подзапрос
WHERE country IN ('RU', 'BY', 'KZ')
WHERE country IN (SELECT code FROM cis_countries)
С литералами всё просто. С подзапросом — всегда коррелируется с другой таблицей или фильтром.
В коде приложения часто пишут IN ($1, $2, $3, ..., $n) — параметры. На больших списках (тысячи значений) лучше передать через временную таблицу или массив, потом сделать подзапрос.
Частые ошибки новичков
1. NOT IN с потенциальным NULL. Уже разобрали. Лучше всегда NOT EXISTS. Если точно уверен, что NULL не будет — добавь явный WHERE column IS NOT NULL в подзапрос.
2. Подзапрос с несколькими колонками. IN (SELECT a, b FROM ...) — НЕ работает в большинстве БД (Postgres поддерживает через (a, b) IN (SELECT ...), MySQL частично, MSSQL — нет). Для портабельности — EXISTS с условиями на нескольких колонках.
3. Подзапрос возвращает дубликаты. Технически не ломает запрос, но избыточен. Если в orders.customer_id каждый клиент попадает много раз, подзапрос возвращает кучу повторов. Postgres всё равно посчитает правильно, но SELECT DISTINCT customer_id или EXISTS чище.
4. Огромный список. IN ('val1', 'val2', ..., 'val10000') — тысяча литералов. Postgres-планировщик потратит много времени на парсинг, и план запроса будет странным. Используй временную таблицу или массив.
5. Путают IN с =. WHERE column = (SELECT ...) подразумевает, что подзапрос вернёт ровно одно значение. Если возвращает несколько — ошибка. Если хочешь «равно одному из» — это IN.
6. Не понимают, что подзапрос выполняется по логике один раз. В отличие от коррелированного EXISTS, подзапрос внутри IN без ссылки на внешнюю таблицу выполняется один раз и возвращает все значения. Postgres кеширует список и проверяет членство.
Мини-резюме
column IN (SELECT ...) — «значение есть в результате подзапроса».
- Безопасно для проверки членства. Хорошо читается.
NOT IN опасен с NULL — может молча вернуть пустой результат. Используй NOT EXISTS вместо него.
- На простых сценариях
IN (subquery) и EXISTS примерно одинаковы по скорости.
- Подзапрос с несколькими колонками —
EXISTS чище и портабельнее.
- Огромные списки — через временную таблицу или массив, не как литералы.
INсам по себе — это «проверка членства»: значение должно быть в списке. У него две формы.С литералами — список значений прямо в запросе:
WHERE country IN ('RU', 'BY', 'KZ')С подзапросом — список вычисляется другим SELECT:
WHERE customer_id IN (SELECT id FROM vip_customers)Вторая форма — гибче и часто нужна в реальных задачах. Эту статью посвятим именно ей.
Зачем нужен IN с подзапросом
Когда список «членов» неизвестен заранее или вычисляется. Сценарии:
Без подзапроса пришлось бы тащить список в код приложения, форматировать его и подставлять — медленнее и грязнее.
Базовый синтаксис
SELECT * FROM таблица_A WHERE column IN ( SELECT column_b FROM таблица_B WHERE условие );Подзапрос возвращает список значений (одна колонка). Внешний запрос проверяет, есть ли значение
columnв этом списке.Пример с таблицами
customers:orders:Запрос «заказы только VIP-клиентов (tier = gold)»:
SELECT * FROM orders WHERE customer_id IN ( SELECT id FROM customers WHERE tier = 'gold' );Подзапрос вернёт
(1, 3)— id-шники Ани и Веры. Внешний запрос оставит только заказы, гдеcustomer_idесть в этом списке:Заказ Боба (#2) отфильтровался — он
free, не VIP.NOT IN — обратная проверка
-- Заказы НЕ-VIP клиентов SELECT * FROM orders WHERE customer_id NOT IN ( SELECT id FROM customers WHERE tier = 'gold' );Логически: «такие, где значения нет в списке». Но тут начинается боль с NULL.
Главная ловушка: NOT IN с NULL
NOT INломается наNULLв подзапросе. Это не баг Postgres — это сама логика SQL про NULL:-- Если подзапрос вернёт ['1', '3', NULL] — внешний WHERE сломается. WHERE id NOT IN (SELECT customer_id FROM orders);Логика SQL:
id NOT IN (1, 3, NULL)— этоid <> 1 AND id <> 3 AND id <> NULL. Последняя часть всегдаUNKNOWN(= не TRUE), и весьANDтожеUNKNOWN. Итог — ничего не выбирается, даже если уidнет совпадений с 1 и 3.Это ловит почти всех новичков: запрос выглядит правильно, тестовые данные не содержат NULL, всё работает на dev. На проде в одной строке появляется NULL — и весь отчёт молча возвращает 0 строк.
Лекарство: всегда фильтровать NULL в подзапросе или использовать
NOT EXISTS:-- Способ 1: отфильтровать NULL в подзапросе WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL); -- Способ 2: NOT EXISTS — рекомендуемый WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = u.id);Правило: для anti-join всегда
NOT EXISTS, забудь проNOT IN.IN vs EXISTS — когда что
-- IN (subquery) WHERE c.id IN (SELECT customer_id FROM orders); -- EXISTS WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);Оба работают. На современных Postgres оптимизатор обычно превращает
IN (subquery)в семантику EXISTS под капотом — производительность похожа.(a,b) IN (SELECT a,b FROM ...)— некрасивоПростое правило: для
IN— пишиIN. ДляNOT IN— пишиNOT EXISTS.IN со списком литералов vs подзапрос
-- Литералы — фиксированный список WHERE country IN ('RU', 'BY', 'KZ') -- Подзапрос — динамический WHERE country IN (SELECT code FROM cis_countries)С литералами всё просто. С подзапросом — всегда коррелируется с другой таблицей или фильтром.
В коде приложения часто пишут
IN ($1, $2, $3, ..., $n)— параметры. На больших списках (тысячи значений) лучше передать через временную таблицу или массив, потом сделать подзапрос.Частые ошибки новичков
1.
NOT INс потенциальным NULL. Уже разобрали. Лучше всегдаNOT EXISTS. Если точно уверен, что NULL не будет — добавь явныйWHERE column IS NOT NULLв подзапрос.2. Подзапрос с несколькими колонками.
IN (SELECT a, b FROM ...)— НЕ работает в большинстве БД (Postgres поддерживает через(a, b) IN (SELECT ...), MySQL частично, MSSQL — нет). Для портабельности —EXISTSс условиями на нескольких колонках.3. Подзапрос возвращает дубликаты. Технически не ломает запрос, но избыточен. Если в
orders.customer_idкаждый клиент попадает много раз, подзапрос возвращает кучу повторов. Postgres всё равно посчитает правильно, ноSELECT DISTINCT customer_idилиEXISTSчище.4. Огромный список.
IN ('val1', 'val2', ..., 'val10000')— тысяча литералов. Postgres-планировщик потратит много времени на парсинг, и план запроса будет странным. Используй временную таблицу или массив.5. Путают
INс=.WHERE column = (SELECT ...)подразумевает, что подзапрос вернёт ровно одно значение. Если возвращает несколько — ошибка. Если хочешь «равно одному из» — этоIN.6. Не понимают, что подзапрос выполняется по логике один раз. В отличие от коррелированного
EXISTS, подзапрос внутриINбез ссылки на внешнюю таблицу выполняется один раз и возвращает все значения. Postgres кеширует список и проверяет членство.Мини-резюме
column IN (SELECT ...)— «значение есть в результате подзапроса».NOT INопасен с NULL — может молча вернуть пустой результат. ИспользуйNOT EXISTSвместо него.IN (subquery)иEXISTSпримерно одинаковы по скорости.EXISTSчище и портабельнее.