Условие
Дана таблица orders(order_id, user_id, dt, amount, status).
- Найдите клиентов, у которых больше 5 заказов в 2025 году.
- Из них оставьте тех, у кого средний чек > 1000.
- Покажите топ-10 по сумме заказов.
Объясните, чем WHERE отличается от HAVING, и где синтаксически разница.
Решение
Подход
WHERE фильтрует строки до агрегации. HAVING — группы после агрегации.
Порядок выполнения логически:
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Запрос
SELECT
user_id,
COUNT(*) AS n_orders,
AVG(amount) AS avg_check,
SUM(amount) AS total_amount
FROM orders
WHERE dt >= '2025-01-01' AND dt < '2026-01-01' -- pre-agg фильтр
AND status = 'paid'
GROUP BY user_id
HAVING COUNT(*) > 5 -- post-agg фильтр
AND AVG(amount) > 1000
ORDER BY total_amount DESC
LIMIT 10;Альтернатива через CTE (читаемее для 3+ условий)
WITH agg AS (
SELECT user_id,
COUNT(*) AS n_orders,
AVG(amount) AS avg_check,
SUM(amount) AS total_amount
FROM orders
WHERE dt >= '2025-01-01' AND dt < '2026-01-01'
AND status = 'paid'
GROUP BY user_id
)
SELECT *
FROM agg
WHERE n_orders > 5 AND avg_check > 1000
ORDER BY total_amount DESC
LIMIT 10;В CTE post-agg фильтр пишется через WHERE — потому что агрегация уже сделана внутри CTE.
WHERE vs HAVING на одном примере
-- считаем только paid: WHERE
-- среди user_id оставляем активных: HAVING
-- если бы написать HAVING status='paid' — ошибка: status вне GROUP BYАлиасы в HAVING
-- Postgres / Snowflake: ОК
SELECT user_id, COUNT(*) AS n
FROM orders GROUP BY user_id
HAVING n > 5; -- алиас работает в Postgres
-- MySQL до 8.0: ошибка, нужно повторить агрегат
HAVING COUNT(*) > 5;COUNT(*) vs COUNT(col)
SELECT user_id,
COUNT(*) AS rows_total, -- считает все строки
COUNT(amount) AS rows_with_amount, -- не считает NULL
COUNT(DISTINCT dt) AS days_active
FROM orders
GROUP BY user_id;Подводные камни
- Фильтр по агрегату в WHERE → ошибка:
WHERE COUNT(*) > 5(агрегаты в WHERE запрещены). - Колонка в SELECT, не в GROUP BY — ошибка в Postgres / стандарте; MySQL <8.0 молча даёт «произвольную строку».
- HAVING без GROUP BY работает: фильтрует одну агрегированную строку.
SELECT COUNT(*) FROM orders HAVING COUNT(*) > 0. DISTINCT+GROUP BY— почти всегда избыточно: GROUP BY и так группирует.COUNT(DISTINCT col)дорого на больших таблицах. ИспользоватьAPPROX_COUNT_DISTINCTв BigQuery /HLL_COUNT_DISTINCTв Snowflake.- Часовые пояса:
dt >= '2025-01-01'зависит от типа dt (date / timestamp / timestamptz). Всегда явноdt::date >= '2025-01-01'. - NULL в GROUP BY — собирается в одну группу (стандарт SQL). Если данных много с NULL — проверить отдельно.
Эталонный ответ
SELECT user_id, COUNT(*) AS n, AVG(amount) AS avg_check, SUM(amount) AS total
FROM orders
WHERE dt >= '2025-01-01' AND dt < '2026-01-01' AND status = 'paid'
GROUP BY user_id
HAVING COUNT(*) > 5 AND AVG(amount) > 1000
ORDER BY total DESC
LIMIT 10;WHERE — фильтр строк до агрегации. HAVING — групп после. Алиасы агрегатов в HAVING работают в Postgres, но не в MySQL. CTE — частая альтернатива HAVING для читаемости.