← все задачи

SQL · задача 4 из 10

Клиенты без заказов

Начальный 10–15 минут LEFT JOINNOT EXISTSNULL

Условие

Таблицы 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 по заказам на каждого клиента

Следующая задача

Накопительный итог по дням — Выручка по дням и нарастающий итог — то, что просят для любого дашборда.