SQLINsubquerytutorial

Что такое IN с подзапросом в SQL?

IN с подзапросом — это «выбери строки, где значение колонки есть в результате другого запроса». Простыми словами: фильтрация по динамическому списку, разница со списком литералов, главная ловушка NOT IN с NULL и сравнение с EXISTS. С таблицами и частыми ошибками.

12 мин чтенияСправочникSQL · IN · subquery · tutorial

IN в SQL отвечает на простой вопрос:

Значение есть в списке или нет?

Самый обычный пример выглядит так:

SELECT *
FROM users
WHERE country IN ('RU', 'BY', 'KZ');

Такой запрос выберет пользователей только из стран RU, BY и KZ.

Но список не всегда известен заранее. Иногда его нужно получить из другой таблицы. Например:

  • выбрать заказы только gold-клиентов;
  • найти комментарии к постам с нужным тегом;
  • показать пользователей, у которых есть платные заказы;
  • выбрать товары из категорий, которые сейчас активны;
  • найти сотрудников из отделов, где больше 10 человек.

Вот для этого используют IN с подзапросом.

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Здесь список клиентов не написан руками. Его вычисляет внутренний SELECT.

Простая идея IN с подзапросом

Есть две формы IN.

Первая — с готовым списком значений:

SELECT *
FROM users
WHERE country IN ('RU', 'US', 'DE');

Вторая — с подзапросом:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

В обоих случаях смысл один:

Оставь строку, если значение слева есть в списке справа.

Разница только в том, откуда берётся список.

В первом случае список написан прямо в запросе: RU, US, DE.

Во втором случае список возвращает другой запрос:

SELECT id
FROM customers
WHERE tier = 'gold'

То есть подзапрос сначала находит нужные значения, а внешний запрос потом использует их как фильтр.

Базовый синтаксис

Общий вид такой:

SELECT columns
FROM table_a
WHERE column_name IN (
  SELECT another_column
  FROM table_b
  WHERE condition
);

Важное правило: подзапрос внутри IN обычно должен возвращать одну колонку.

Например, так правильно:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

А так нельзя, если ты используешь обычный IN для одной колонки:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id, name
  FROM customers
);

Почему нельзя?

Потому что слева одно значение — customer_id. А справа подзапрос возвращает сразу две колонки: id и name. SQL не понимает, с чем именно сравнивать customer_id.

Пример с таблицами customers и orders

Допустим, есть таблица customers:

id name tier
1 Anna gold
2 Bob free
3 Vera gold
4 Gleb free

И таблица orders:

id customer_id amount
1 1 100
2 2 50
3 1 200
4 3 300

Нужно выбрать заказы только gold-клиентов.

Сначала посмотрим, какой список вернёт подзапрос:

SELECT id
FROM customers
WHERE tier = 'gold';

Результат:

id
1
3

То есть gold-клиенты — это клиенты с id 1 и 3.

Теперь используем этот список во внешнем запросе:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Результат:

id customer_id amount
1 1 100
3 1 200
4 3 300

Заказ клиента 2 не попал в результат, потому что клиент 2 имеет тариф free, а не gold.

Как SQL читает такой запрос

Запрос:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Можно мысленно прочитать так:

  1. Найди всех клиентов с тарифом gold.
  2. Возьми их id.
  3. В таблице orders оставь только те заказы, где customer_id входит в этот список.

То есть подзапрос превращается в динамический список.

Условно это похоже на такой запрос:

SELECT *
FROM orders
WHERE customer_id IN (1, 3);

Только значения 1 и 3 мы не писали руками. Их нашла база.

Зачем нужен IN с подзапросом

Главная польза — не нужно заранее знать список значений.

Например, сегодня gold-клиенты — это 1 и 3, завтра появится клиент 8, послезавтра клиент 12 сменит тариф.

Если писать список руками, запрос быстро устареет:

SELECT *
FROM orders
WHERE customer_id IN (1, 3);

А вариант с подзапросом всегда берёт актуальные данные из таблицы:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Это особенно удобно, когда список:

  • большой;
  • часто меняется;
  • зависит от условий;
  • должен вычисляться по другой таблице;
  • не должен собираться в коде приложения.

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

Пример: пользователи с платными заказами

Допустим, есть таблица users:

id name
1 Anna
2 Bob
3 Vera
4 Gleb

И таблица orders:

id user_id status amount
1 1 paid 100
2 1 canceled 50
3 3 paid 200
4 4 pending 300

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

SELECT *
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
  WHERE status = 'paid'
);

Подзапрос вернёт:

user_id
1
3

Внешний запрос оставит пользователей с id 1 и 3.

Результат:

id name
1 Anna
3 Vera

Логика простая:

Покажи пользователей, чей id встречается среди user_id оплаченных заказов.

Пример: посты с нужным тегом

Допустим, есть таблица posts:

id title
1 SQL basics
2 Python tips
3 SQL joins

Есть таблица связей post_tags:

post_id tag
1 sql
1 beginner
2 python
3 sql

Нужно выбрать посты с тегом sql.

SELECT *
FROM posts
WHERE id IN (
  SELECT post_id
  FROM post_tags
  WHERE tag = 'sql'
);

Результат:

id title
1 SQL basics
3 SQL joins

Подзапрос нашёл post_id, у которых есть тег sql, а внешний запрос достал сами посты.

IN и дубликаты в подзапросе

Подзапрос внутри IN может вернуть дубликаты. Например:

SELECT user_id
FROM orders
WHERE status = 'paid';

Результат может быть таким:

user_id
1
1
1
3

Для IN это не ломает логику.

Запрос:

SELECT *
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
  WHERE status = 'paid'
);

всё равно выберет пользователя 1 один раз, а не три раза.

Почему?

Потому что IN проверяет членство в списке: есть значение в списке или нет. Ему не важно, сколько раз оно там встретилось.

Но дубликаты могут быть лишней работой для базы. Иногда запрос можно сделать аккуратнее через DISTINCT:

SELECT *
FROM users
WHERE id IN (
  SELECT DISTINCT user_id
  FROM orders
  WHERE status = 'paid'
);

А иногда ещё лучше использовать EXISTS. Об этом поговорим ниже.

NOT IN: обратная проверка

У IN есть обратный вариант — NOT IN.

IN означает:

Значение есть в списке.

NOT IN означает:

Значения нет в списке.

Например, нужно выбрать заказы не gold-клиентов:

SELECT *
FROM orders
WHERE customer_id NOT IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Если подзапрос вернул список 1 и 3, то внешний запрос оставит заказы клиентов, чей customer_id не равен ни 1, ни 3.

На наших данных результат будет таким:

id customer_id amount
2 2 50

Клиент 2 не gold, поэтому его заказ остался.

Но у NOT IN есть очень опасная ловушка.

Главная ловушка: NOT IN и NULL

NOT IN может неожиданно вернуть пустой результат, если в подзапросе есть NULL.

Это одна из самых неприятных ловушек SQL для новичков.

Допустим, есть таблица users:

id name
1 Anna
2 Bob
3 Vera

И таблица orders:

id user_id
1 1
2 NULL

Ты хочешь найти пользователей, у которых нет заказов.

Пишешь:

SELECT *
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
);

На первый взгляд логика нормальная:

Покажи пользователей, чей id не встречается среди user_id в заказах.

Но подзапрос возвращает:

user_id
1
NULL

И условие превращается примерно в такую проверку:

WHERE id NOT IN (1, NULL)

Для пользователя с id = 2 это означает:

2 <> 1 AND 2 <> NULL

Первая часть — TRUE.

А вторая часть — не TRUE, а UNKNOWN, потому что сравнение с NULL не даёт обычный ответ «да» или «нет».

В SQL NULL означает неизвестное значение. Поэтому выражение 2 <> NULL не является истинным.

В итоге строка не проходит фильтр.

И так может не пройти вообще никто.

Самое неприятное: запрос не падает с ошибкой. Он просто возвращает пустой результат.

Как безопасно писать NOT IN

Есть два способа.

Первый способ — явно убрать NULL из подзапроса:

SELECT *
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
  WHERE user_id IS NOT NULL
);

Теперь подзапрос не вернёт NULL, и NOT IN будет вести себя ожидаемо.

Но в реальных задачах для такой логики чаще советуют использовать NOT EXISTS.

SELECT *
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Этот запрос читается так:

Покажи пользователя, для которого не существует ни одного заказа с таким user_id.

Это безопаснее при NULL и часто лучше читается для анти-проверок.

Практическое правило:

Для IN можно использовать IN.
Для обратной проверки чаще используй NOT EXISTS, а не NOT IN.

IN и EXISTS: в чём разница

Для проверки «есть ли связанные строки» часто можно написать запрос двумя способами.

Через IN:

SELECT *
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
);

Через EXISTS:

SELECT *
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
);

Оба запроса отвечают на один вопрос:

Покажи клиентов, у которых есть хотя бы один заказ.

Но стиль разный.

IN говорит:

Возьми список customer_id из заказов и проверь, входит ли customers.id в этот список.

EXISTS говорит:

Для каждого клиента проверь, существует ли хотя бы один заказ этого клиента.

Когда удобнее IN

IN хорошо читается, когда подзапрос возвращает один простой список значений.

Например:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Или:

SELECT *
FROM products
WHERE category_id IN (
  SELECT id
  FROM categories
  WHERE is_active = true
);

Это выглядит естественно:

Значение должно быть в списке.

Если задача именно такая, IN — хороший выбор.

Когда удобнее EXISTS

EXISTS удобнее, когда условие связано с несколькими колонками или когда нужна проверка существования строки.

Например, нужно найти пользователей, у которых есть оплаченный заказ дороже 1000.

SELECT *
FROM users u
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
    AND o.status = 'paid'
    AND o.amount > 1000
);

Можно написать похожее через IN, но EXISTS здесь читается яснее:

Покажи пользователя, если существует подходящий заказ.

Особенно хорошо EXISTS подходит для NOT EXISTS:

SELECT *
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Это понятный способ найти пользователей без заказов.

IN или JOIN

Иногда ту же задачу можно решить через JOIN.

Например, заказы gold-клиентов через IN:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

А вот вариант через JOIN:

SELECT o.*
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.tier = 'gold';

Оба варианта могут дать один и тот же результат.

Как выбрать?

IN хорош, когда тебе нужны строки только из внешней таблицы, а вторая таблица нужна просто как фильтр.

Например:

Покажи заказы клиентов из нужного сегмента.

JOIN хорош, когда тебе нужны колонки из обеих таблиц.

Например:

SELECT
  o.id,
  o.amount,
  c.name,
  c.tier
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.tier = 'gold';

Здесь уже нужны и данные заказа, и данные клиента. Поэтому JOIN выглядит естественнее.

IN с подзапросом и несколько колонок

Обычный IN сравнивает одно значение с одним списком.

WHERE customer_id IN (
  SELECT id
  FROM customers
)

Иногда нужно сравнить пару значений. В PostgreSQL можно использовать такую форму:

SELECT *
FROM order_items
WHERE (order_id, product_id) IN (
  SELECT order_id, product_id
  FROM returned_items
);

Здесь сравнивается пара order_id + product_id.

Но такая запись хуже переносится между разными базами данных и может быть непривычной для новичков.

Часто понятнее написать через EXISTS:

SELECT *
FROM order_items oi
WHERE EXISTS (
  SELECT 1
  FROM returned_items ri
  WHERE ri.order_id = oi.order_id
    AND ri.product_id = oi.product_id
);

Так явно видно, какие колонки с какими сравниваются.

IN с пустым результатом подзапроса

Подзапрос может вернуть пустой список.

Например:

SELECT id
FROM customers
WHERE tier = 'diamond';

Если клиентов с таким тарифом нет, подзапрос вернёт 0 строк.

Тогда внешний запрос:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'diamond'
);

тоже вернёт 0 строк.

Это логично: значение не может входить в пустой список.

А вот с NOT IN пустой список работает наоборот:

SELECT *
FROM orders
WHERE customer_id NOT IN (
  SELECT id
  FROM customers
  WHERE tier = 'diamond'
);

Если подзапрос вернул пустой список, то условие означает:

customer_id не входит в пустой список.

Это будет верно для всех строк, где сама проверка не упирается в NULL.

Но снова помним: если в списке появляется NULL, у NOT IN начинаются проблемы.

IN с большим списком значений

Иногда в приложении собирают огромный запрос:

SELECT *
FROM users
WHERE id IN (1, 2, 3, 4, 5);

Для маленького списка это нормально.

Но если значений тысячи или десятки тысяч, запрос становится неудобным:

  • его сложнее читать;
  • база тратит время на разбор длинного SQL;
  • план запроса может стать хуже;
  • приложение начинает заниматься работой, которую лучше сделать в базе.

В таких случаях часто лучше:

  • положить значения во временную таблицу;
  • использовать отдельную таблицу со списком;
  • передать массив, если это удобно в выбранной СУБД;
  • сделать JOIN или подзапрос.

Идея простая: огромный список литералов внутри SQL — обычно не лучший формат для данных.

Частые ошибки новичков

Ошибка 1. Использовать NOT IN с подзапросом, где может быть NULL

Плохой вариант:

SELECT *
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
);

Если orders.user_id содержит NULL, запрос может вернуть пустой результат.

Безопаснее так:

SELECT *
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
  WHERE user_id IS NOT NULL
);

А чаще ещё лучше так:

SELECT *
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Ошибка 2. Вернуть несколько колонок в подзапросе

Плохой вариант:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id, name
  FROM customers
);

Подзапрос для обычного IN должен вернуть одну колонку.

Правильно:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
);

Если нужно сравнивать несколько колонок, часто лучше использовать EXISTS.

Ошибка 3. Путать IN и равно

Такой запрос ожидает, что подзапрос вернёт ровно одно значение:

SELECT *
FROM orders
WHERE customer_id = (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Если gold-клиентов несколько, будет ошибка.

Если значений может быть много, нужен IN:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

= — для одного значения.

IN — для списка значений.

Ошибка 4. Забыть, что IN не умножает строки

Допустим, подзапрос вернул одного и того же клиента три раза:

user_id
1
1
1

Запрос:

SELECT *
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
);

не вернёт пользователя три раза. Он просто проверит, есть ли id в списке.

Если тебе нужно получить по строке на каждый заказ, нужен JOIN, а не IN.

Ошибка 5. Использовать IN там, где нужен JOIN

Если тебе нужны только пользователи, у которых есть заказы, IN подходит:

SELECT *
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
);

Но если нужно вывести ещё и сумму заказа, дату заказа или статус заказа, нужен JOIN:

SELECT
  u.id,
  u.name,
  o.amount,
  o.status
FROM users u
JOIN orders o ON o.user_id = u.id;

IN фильтрует. JOIN соединяет таблицы и позволяет брать колонки из обеих.

Ошибка 6. Писать огромный IN-список руками

Плохо:

SELECT *
FROM users
WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Для небольшого списка нормально. Но если значений тысячи, лучше подумать о временной таблице, массиве, JOIN или подзапросе.

Ошибка 7. Не учитывать NULL слева

Если значение слева само NULL, проверка через IN не даст TRUE.

Например:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
);

Если у заказа customer_id равен NULL, такая строка не пройдёт фильтр. SQL не может сказать, что неизвестный клиент точно входит в список.

Если такие строки нужны отдельно, их надо обработать явно:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
)
   OR customer_id IS NULL;

Как читать IN с подзапросом

Возьмём запрос:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
    AND is_active = true
);

Читать его удобно так:

  1. В таблице customers найди активных клиентов с тарифом gold.
  2. Возьми их id.
  3. В таблице orders оставь только заказы этих клиентов.

То есть сначала мысленно разбираем подзапрос, потом внешний запрос.

Ещё пример:

SELECT *
FROM products
WHERE category_id IN (
  SELECT id
  FROM categories
  WHERE is_active = true
);

Читается так:

Покажи товары, у которых категория входит в список активных категорий.

Когда использовать IN с подзапросом

Используй IN с подзапросом, когда условие звучит так:

  • значение должно входить в список из другой таблицы;
  • список заранее неизвестен;
  • список можно получить отдельным SELECT;
  • внешней таблице нужны только свои колонки;
  • подзапрос возвращает одну колонку.

Хорошие примеры:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);
SELECT *
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
  WHERE status = 'paid'
);
SELECT *
FROM products
WHERE category_id IN (
  SELECT id
  FROM categories
  WHERE is_active = true
);

Во всех этих запросах IN читается естественно: значение должно быть среди значений, найденных подзапросом.

Когда лучше выбрать EXISTS

Выбирай EXISTS, когда:

  • нужно проверить существование связанной строки;
  • условие зависит от нескольких колонок;
  • пишешь обратную проверку через NOT EXISTS;
  • хочешь избежать ловушек NOT IN с NULL.

Например:

SELECT *
FROM users u
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
    AND o.status = 'paid'
);

И особенно:

SELECT *
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Для поиска строк «без пары» NOT EXISTS обычно самый понятный и безопасный вариант.

Главное из статьи

IN с подзапросом проверяет, входит ли значение в список, который вернул другой SELECT.

Базовый пример:

SELECT *
FROM orders
WHERE customer_id IN (
  SELECT id
  FROM customers
  WHERE tier = 'gold'
);

Главные правила:

  • IN означает «значение есть в списке».
  • Подзапрос внутри обычного IN должен возвращать одну колонку.
  • IN удобен, когда список значений заранее неизвестен и вычисляется из другой таблицы.
  • Дубликаты в подзапросе не меняют результат IN.
  • NOT IN опасен, если подзапрос может вернуть NULL.
  • Для обратной проверки часто лучше использовать NOT EXISTS.
  • Если нужно вывести колонки из двух таблиц, чаще нужен JOIN, а не IN.
  • Если подзапрос может вернуть много значений, это нормально, но огромные списки литералов лучше заменять таблицей, массивом или подзапросом.
  • = подходит для одного значения, IN — для списка значений.
  • Если нужно сравнить несколько колонок, часто понятнее использовать EXISTS.

Если сказать совсем просто: IN с подзапросом — это способ сказать базе: «Сначала найди подходящий список значений, а потом оставь только строки, которые в этот список входят».

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр