Задачи на подзапросы и CTE
Подзапросы и CTE·middle

Клиенты с тратами выше среднего

Задача

Отдел лояльности ищет ядро аудитории: «Дай клиентов, которые потратили больше, чем в среднем тратит клиент. Не выше среднего чека, а выше средних суммарных трат на человека — это наши самые ценные».

Верни имя, фамилию и суммарные траты клиентов, у которых эта сумма выше средней по всем клиентам.

Схема данных

Учебная база проката dvdrental:

  • customer(customer_id, first_name, last_name, ...) — клиенты.
  • payment(payment_id, customer_id, amount, ...) — платежи; у клиента их много.

Решение

WITH spend AS (
  SELECT c.customer_id, c.first_name, c.last_name,
         sum(p.amount) AS total_spent
  FROM payment p
  JOIN customer c USING (customer_id)
  GROUP BY c.customer_id, c.first_name, c.last_name
)
SELECT first_name, last_name, total_spent
FROM spend
WHERE total_spent > (SELECT avg(total_spent) FROM spend)
ORDER BY total_spent DESC, customer_id;

Разбор

Здесь легко попасть в ловушку формулировки. «Выше среднего» — но среднего чего? Среднего чека (одного платежа) или средних трат на человека? Задача про второе: сначала сворачиваем каждого клиента в его итог, а потом сравниваем эти итоги между собой. Это два разных «средних», и путаница между ними — классическая ошибка на собеседовании.

Задача двухэтажная, и WITH (CTE) делает её читаемой. CTE spend — это как временная именованная таблица, живущая только внутри запроса: она считает суммарные траты каждого клиента через обычный GROUP BY. Дальше основной запрос работает уже с этой таблицей итогов, будто она настоящая. Когда брать CTE, а когда обычный подзапрос или временную таблицу, — в вопросе подзапрос, CTE или временная таблица.

Самое приятное — spend можно переиспользовать дважды. Один раз мы из неё выбираем строки, второй раз — считаем по ней среднее в подзапросе (SELECT avg(total_spent) FROM spend). Без CTE пришлось бы дублировать всю агрегацию из платежей в двух местах: и в основном запросе, и в подзапросе для среднего. CTE убирает этот копипаст — тот же расчёт описан один раз.

Логика фильтра: подзапрос отдаёт одно число — средний итог по всем клиентам, — а WHERE total_spent > <это число> оставляет тех, кто выше. Здесь важно, что сравнение идёт уже после агрегации, поэтому оно и вынесено на верхний уровень, а не в HAVING: мы фильтруем строки готовой витрины spend, а не группы во время их сборки.

ORDER BY total_spent DESC, customer_id даёт стабильный порядок — сначала самые щедрые, tie-break по customer_id.

Если нужно не просто отобрать, а проставить клиентам места, — смотри задачу ранжирование клиентов по тратам.

Ожидаемый результат

first_name | last_name | total_spent
--- | --- | ---
Eleanor | Hunt | 211.55
Karl | Seal | 208.58
Marion | Snyder | 194.61
Rhonda | Kennedy | 191.62
Clara | Shaw | 189.60
Tommy | Collazo | 183.63
Ana | Bradley | 167.67
Curtis | Irby | 167.62
Marcia | Dean | 166.61
Mike | Way | 162.67
… всего строк: 285 (показаны первые 10)

Прорешай такие задачи сам

Читать разбор полезно, а настоящая прокачка — самому писать запросы и получать обратную связь. В симуляторе живая база и проверка запросов, лёгкий режим — бесплатно.

Прорешать в бесплатной песочнице

Смежные задачи

Вопросы по теме