Подзапросы и CTE

Подзапросы: скалярный и в WHERE

Содержание курса

Подзапрос в IN/NOT IN: фильтрация по списку и ловушка NULL

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

Предположим, нужно найти клиентов, у которых есть хотя бы один заказ дороже 3000:

SELECT name
FROM customers
WHERE customer_id IN (
    SELECT customer_id
    FROM orders
    WHERE total_amount > 3000
);

Внутренний запрос возвращает список customer_id — строк может быть сколько угодно, один столбец. Внешний запрос проверяет каждую строку customers: если её customer_id есть в этом списке, строка включается в результат. Один клиент попадёт в результат ровно один раз, даже если у него несколько подходящих заказов — это отличает IN от JOIN, который мог бы дублировать строку клиента.

NOT IN инвертирует логику: остаются клиенты, чей customer_id отсутствует в списке. Запрос «клиенты без ни одного заказа»:

SELECT name
FROM customers
WHERE customer_id NOT IN (
    SELECT customer_id
    FROM orders
);

Ловушка: NULL в списке. Если подзапрос вернул хотя бы одну строку с NULL, NOT IN перестаёт работать так, как ожидается. Если проверяемое значение совпало с одним из ненулевых элементов списка, NOT IN даёт FALSE. Если совпадения нет, но в списке присутствует NULL, результат становится UNKNOWN: SQL не может доказать, что значение отличается от неизвестного элемента. Ни FALSE, ни UNKNOWN не проходят фильтр WHERE, поэтому при наличии NULL в списке NOT IN может отбросить все строки — без ошибки и предупреждения.

Защита простая: добавить WHERE customer_id IS NOT NULL внутрь подзапроса:

SELECT name
FROM customers
WHERE customer_id NOT IN (
    SELECT customer_id
    FROM orders
    WHERE customer_id IS NOT NULL
);

Для IN ловушка менее критична: NULL в списке просто не даёт совпадения, но остальные значения проверяются нормально. С NOT IN она фатальна, поэтому привычка добавлять IS NOT NULL внутрь подзапроса — хорошая страховка в обоих случаях.