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';
Результат:
То есть 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'
);
Можно мысленно прочитать так:
- Найди всех клиентов с тарифом
gold.
- Возьми их
id.
- В таблице
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'
);
Подзапрос вернёт:
Внешний запрос оставит пользователей с id 1 и 3.
Результат:
Логика простая:
Покажи пользователей, чей 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';
Результат может быть таким:
Для 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:
Ты хочешь найти пользователей, у которых нет заказов.
Пишешь:
SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM orders
);
На первый взгляд логика нормальная:
Покажи пользователей, чей id не встречается среди user_id в заказах.
Но подзапрос возвращает:
И условие превращается примерно в такую проверку:
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 не умножает строки
Допустим, подзапрос вернул одного и того же клиента три раза:
Запрос:
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
);
Читать его удобно так:
- В таблице
customers найди активных клиентов с тарифом gold.
- Возьми их
id.
- В таблице
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 с подзапросом — это способ сказать базе: «Сначала найди подходящий список значений, а потом оставь только строки, которые в этот список входят».
INв SQL отвечает на простой вопрос:Самый обычный пример выглядит так:
SELECT * FROM users WHERE country IN ('RU', 'BY', 'KZ');Такой запрос выберет пользователей только из стран
RU,BYиKZ.Но список не всегда известен заранее. Иногда его нужно получить из другой таблицы. Например:
gold-клиентов;Вот для этого используют
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:И таблица
orders:Нужно выбрать заказы только
gold-клиентов.Сначала посмотрим, какой список вернёт подзапрос:
SELECT id FROM customers WHERE tier = 'gold';Результат:
То есть
gold-клиенты — это клиенты сid1 и 3.Теперь используем этот список во внешнем запросе:
SELECT * FROM orders WHERE customer_id IN ( SELECT id FROM customers WHERE tier = 'gold' );Результат:
Заказ клиента
2не попал в результат, потому что клиент2имеет тарифfree, а неgold.Как SQL читает такой запрос
Запрос:
SELECT * FROM orders WHERE customer_id IN ( SELECT id FROM customers WHERE tier = 'gold' );Можно мысленно прочитать так:
gold.id.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:И таблица
orders:Нужно найти пользователей, у которых есть хотя бы один оплаченный заказ.
SELECT * FROM users WHERE id IN ( SELECT user_id FROM orders WHERE status = 'paid' );Подзапрос вернёт:
Внешний запрос оставит пользователей с
id1 и 3.Результат:
Логика простая:
Пример: посты с нужным тегом
Допустим, есть таблица
posts:Есть таблица связей
post_tags:Нужно выбрать посты с тегом
sql.SELECT * FROM posts WHERE id IN ( SELECT post_id FROM post_tags WHERE tag = 'sql' );Результат:
Подзапрос нашёл
post_id, у которых есть тегsql, а внешний запрос достал сами посты.IN и дубликаты в подзапросе
Подзапрос внутри
INможет вернуть дубликаты. Например:SELECT user_id FROM orders WHERE status = 'paid';Результат может быть таким:
Для
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.На наших данных результат будет таким:
Клиент
2неgold, поэтому его заказ остался.Но у
NOT INесть очень опасная ловушка.Главная ловушка: NOT IN и NULL
NOT INможет неожиданно вернуть пустой результат, если в подзапросе естьNULL.Это одна из самых неприятных ловушек SQL для новичков.
Допустим, есть таблица
users:И таблица
orders:Ты хочешь найти пользователей, у которых нет заказов.
Пишешь:
SELECT * FROM users WHERE id NOT IN ( SELECT user_id FROM orders );На первый взгляд логика нормальная:
Но подзапрос возвращает:
И условие превращается примерно в такую проверку:
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 );Этот запрос читается так:
Это безопаснее при
NULLи часто лучше читается для анти-проверок.Практическое правило:
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говорит: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' );Если подзапрос вернул пустой список, то условие означает:
Это будет верно для всех строк, где сама проверка не упирается в
NULL.Но снова помним: если в списке появляется
NULL, уNOT INначинаются проблемы.IN с большим списком значений
Иногда в приложении собирают огромный запрос:
SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5);Для маленького списка это нормально.
Но если значений тысячи или десятки тысяч, запрос становится неудобным:
В таких случаях часто лучше:
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 не умножает строки
Допустим, подзапрос вернул одного и того же клиента три раза:
Запрос:
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 );Читать его удобно так:
customersнайди активных клиентов с тарифомgold.id.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с подзапросом — это способ сказать базе: «Сначала найди подходящий список значений, а потом оставь только строки, которые в этот список входят».