Условие
Таблицы customers и orders. Найдите клиентов, у которых нет ни одного заказа. Покажите несколько способов и скажите, какой предпочтёте.
Что требуется
- Результат — только клиенты без заказов
- Запрос должен работать, если в orders есть строки с customer_id IS NULL
- Объясните разницу способов
Пример
customers: 1 Иван, 2 Пётр, 3 Анна orders: (1, ...), (1, ...), (3, ...) -> Пётр
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Совсем без заказов или без заказов за период?
- Считаются ли отменённые заказы — возможно, нужен фильтр по статусу
- Может ли customer_id в orders быть NULL?
Показать решение Скрыть решение
Решение
-- 1. NOT EXISTS — предпочтительный вариант
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- 2. LEFT JOIN с проверкой на NULL — «антиджойн» руками
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
-- 3. NOT IN — работает, пока в подзапросе нет NULL
SELECT c.id, c.name
FROM customers c
WHERE c.id NOT IN (
SELECT customer_id FROM orders WHERE customer_id IS NOT NULL
);
Почему так
Чем опасен NOT IN
- Если подзапрос вернёт хотя бы один NULL, результат всего NOT IN станет неопределённым и запрос вернёт ноль строк
- Это происходит молча: запрос «работает», просто ничего не находит — и баг живёт месяцами
- Поэтому в третьем варианте явный IS NOT NULL, а лучше не использовать NOT IN на nullable-колонках вовсе
Почему NOT EXISTS обычно лучше
- Он честно выражает намерение: «не существует ни одной такой строки»
- Планировщик разворачивает его в anti-join и останавливается на первом совпадении, а не собирает все заказы клиента
- С NULL он ведёт себя предсказуемо, в отличие от NOT IN
Почему в LEFT JOIN проверяют o.id, а не o.customer_id
- Проверять нужно колонку, которая гарантированно не NULL у существующей строки, — обычно это первичный ключ
- Если проверить nullable-колонку, строки с NULL в ней попадут в результат ошибочно
- И ещё: при таком join каждый клиент с сотней заказов породит сто строк, которые потом отфильтруются, — лишняя работа
Что спросят дальше
- Спросят про «без заказов за последний год» — условие по дате должно уйти внутрь EXISTS или в ON, а не в WHERE
- Условие в WHERE после LEFT JOIN превращает его в INNER JOIN — классическая ловушка, и её любят проверять
- Спросят про индекс orders(customer_id) — без него это будет seq scan по заказам на каждого клиента
Следующая задача
Накопительный итог по дням — Выручка по дням и нарастающий итог — то, что просят для любого дашборда.