Даны таблицы orders (id, customer_id, amount, created_at) и customers (id, name). Напишите запрос: топ-5 клиентов по сумме заказов за последние 30 дней, включая число заказов.
Короткий ответ
- JOIN customers и orders по customer_id
- Фильтр по дате в WHERE до агрегации
- GROUP BY по клиенту, SUM и COUNT как агрегаты
- Сортировка по сумме убыванием, LIMIT 5
- Условия на агрегаты шли бы в HAVING, не в WHERE
- Индекс по (customer_id, created_at) ускорит выборку
Классическая связка JOIN + WHERE по дате + GROUP BY + ORDER BY + LIMIT; важно не путать WHERE и HAVING.
Как сказать вслух
пример ответаСоединяю заказы с клиентами по customer_id, в WHERE отфильтровываю последние тридцать дней — это важно сделать до группировки. Дальше группирую по клиенту, беру сумму и количество заказов, сортирую по сумме по убыванию и ограничиваю пятью строками. Если бы нужно было отобрать клиентов, например, с суммой больше ста тысяч, это условие пошло бы в HAVING, потому что WHERE с агрегатами не работает.
Подробный ответ
Основной ответ
Задача проверяет базовую механику SQL: порядок логического выполнения (FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT), понимание, что фильтр по дате — это условие на строки и должен стоять в WHERE, а условия на результаты агрегатов — в HAVING. GROUP BY должен включать все неагрегированные колонки из SELECT (c.id, c.name); группировка по c.id корректна, так как name функционально зависит от первичного ключа. INNER JOIN здесь уместен: клиенты без заказов в топ не попадут по определению; если бы требовалось показать всех клиентов, понадобился бы LEFT JOIN с COALESCE(SUM(amount), 0). Для производительности на большой таблице нужен индекс по (customer_id, created_at) или хотя бы по created_at — иначе фильтр за 30 дней обернётся полным сканом.
Ключевые моменты
- WHERE vs HAVING. WHERE фильтрует строки до группировки, HAVING — группы после. Фильтр по дате в HAVING формально сработает, но семантически неверен.
- GROUP BY и SELECT. Каждая колонка SELECT либо агрегат, либо в GROUP BY; группировка по первичному ключу покрывает зависимые колонки.
- Выбор JOIN. INNER отбрасывает клиентов без заказов — для топа это правильно; на вопрос «а все клиенты с нулями?» ответ — LEFT JOIN.
Практический контекст
Такой запрос — хлеб повседневной работы: отчёты, админки, дашборды. На собеседовании его дают, чтобы за пять минут увидеть, пишет ли кандидат SQL руками или только через ORM. Частые дополнительные вопросы: куда поставить условие на сумму (HAVING), какой индекс поможет, как изменится запрос, если нужны и клиенты без заказов.
Пример кода
SELECT c.id,
c.name,
SUM(o.amount) AS total_amount,
COUNT(o.id) AS orders_count
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.created_at >= now() - INTERVAL '30 days'
GROUP BY c.id, c.name
ORDER BY total_amount DESC
LIMIT 5;Частые ошибки
- Ставят фильтр по дате в HAVING или условие на SUM в WHERE
- Забывают включить неагрегированные колонки в GROUP BY
- Используют LEFT JOIN там, где он не нужен, или не могут объяснить выбор