GROUP BY SQL: группировка, агрегаты и HAVING
GROUP BY SQL используется, когда строки нужно объединить в группы и посчитать для каждой из них агрегаты: количество, сумму, среднее, минимум или максимум. На примере разберём группировку магазинов, условные агрегаты и отличие HAVING от WHERE.
Обновлено · Команда SQL Arena
Что потренируете
- COUNT, SUM, AVG, MIN и MAX
- GROUP BY по ключу и атрибуту
- WHERE до агрегации и HAVING после неё
- COUNT(*) против COUNT(col) и COUNT(DISTINCT col)
- условная агрегация через FILTER и CASE
Пример задачи в тренажёре
Так выглядит задача в SQL Arena: условие, редактор, результат и схема базы.
Есть магазины stores и их заказы orders. Для каждого магазина посчитайте только оплаченные заказы. В результат должны попасть магазины, у которых не меньше двух оплаченных заказов. Выведите название как store_name, количество как paid_orders и сумму оплат как paid_revenue.
SELECT s.store_name, COUNT(*) FILTER (WHERE o.status = 'paid') AS paid_orders, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS paid_revenue FROM stores AS s JOIN orders AS o ON o.store_id = s.id GROUP BY s.id, s.store_name HAVING COUNT(*) FILTER (WHERE o.status = 'paid') >= 2 ORDER BY paid_revenue DESC, s.store_name;
| # | store_name | paid_orders | paid_revenue |
|---|---|---|---|
| 1 | Paper Moon | 3 | 350 |
| 2 | Blue Shelf | 2 | 270 |
| 3 | North Books | 2 | 200 |
Есть магазины stores и их заказы orders. Для каждого магазина посчитайте только оплаченные заказы. В результат должны попасть магазины, у которых не меньше двух оплаченных заказов. Выведите название как store_name, количество как paid_orders и сумму оплат как paid_revenue.
SELECT s.store_name, COUNT(*) FILTER (WHERE o.status = 'paid') AS paid_orders, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS paid_revenue FROM stores AS s JOIN orders AS o ON o.store_id = s.id GROUP BY s.id, s.store_name HAVING COUNT(*) FILTER (WHERE o.status = 'paid') >= 2 ORDER BY paid_revenue DESC, s.store_name;
Как работает GROUP BY SQL вместе с агрегатами
После JOIN каждая строка результата соответствует одному заказу вместе с данными его магазина. Затем GROUP BY s.id, s.store_name собирает заказы обратно в группы по магазинам. Важно группировать по s.id, а не только по названию: два разных магазина теоретически могут называться одинаково. Название добавлено в GROUP BY, потому что оно выводится в SELECT и само не является агрегатом.
Оба агрегата используют FILTER (WHERE o.status = 'paid'). Поэтому отменённый заказ остаётся в исходной группе, но не участвует ни в paid_orders, ни в paid_revenue. COUNT(*) FILTER (...) считает оплаченные строки, а SUM(o.amount) FILTER (...) складывает их суммы. В PostgreSQL FILTER часто читается яснее, чем несколько конструкций SUM(CASE WHEN ... THEN ... END), особенно когда в одном отчёте нужно получить несколько условных метрик.
Последний шаг — HAVING COUNT(*) FILTER (...) >= 2. HAVING применяется уже к сформированным группам, поэтому здесь можно отфильтровать магазины по результату агрегата. North Books остаётся с двумя оплатами на 200, Paper Moon — с тремя на 350, Blue Shelf — с двумя на 270. Ink House имеет только одну оплату, а Page Corner — ни одной, поэтому они не проходят HAVING. ORDER BY paid_revenue DESC, s.store_name сначала ставит выше магазины с большей выручкой, а второй ключ задаёт стабильный порядок при равенстве.
SELECT s.store_name, COUNT(*) FILTER (WHERE o.status = 'paid') AS paid_orders, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS paid_revenue FROM stores AS s JOIN orders AS o ON o.store_id = s.id GROUP BY s.id, s.store_name HAVING COUNT(*) FILTER (WHERE o.status = 'paid') >= 2 ORDER BY paid_revenue DESC, s.store_name;
| store_name | paid_orders | paid_revenue |
|---|---|---|
| Paper Moon | 3 | 350 |
| Blue Shelf | 2 | 270 |
| North Books | 2 | 200 |
Ошибки в GROUP BY и HAVING SQL
Агрегат в WHERE вместо HAVING
Условие COUNT(*) >= 2 нельзя проверять обычным WHERE, потому что WHERE работает до формирования групп. Ограничения на отдельные строки относятся к WHERE, а ограничения на COUNT, SUM, AVG и другие результаты группировки — к HAVING.
WHERE status = 'paid' вместо условной агрегации
В этой конкретной задаче предварительный WHERE тоже позволил бы получить нужные три строки, но он полностью удалил бы отменённые заказы до группировки. Это становится проблемой, как только в том же SELECT нужно одновременно посчитать оплаченные и отменённые заказы. FILTER или SUM(CASE ...) позволяют считать разные условия внутри одной группы.
COUNT(column) без понимания NULL и DISTINCT
COUNT(*) считает строки, COUNT(column) — только строки, где конкретная колонка не NULL, а COUNT(DISTINCT column) — число различных ненулевых значений. Эти выражения отвечают на разные вопросы. Подмена одного другим часто проходит на маленьком примере и ломается на скрытых данных.
Задачи по теме в тренажёре
Трек «SQL с нуля → Аналитик»Дальше полезно менять не только таблицы, но и сам тип агрегации. В SQL Arena есть задачи на определение активности по времени, построение набора товаров пользователя и подсчёт уникальных товаров, а на следующем уровне — аудиторию платформ, историю действий и помесячную выручку. Так приходится выбирать между COUNT, COUNT DISTINCT, SUM, массивами, условными агрегатами и разными уровнями группировки, а не повторять один шаблон. В тренажёре 1 192 задачи с автопроверкой, причём у каждой задачи собственная схема. Решение дополнительно проверяется на скрытых данных, поэтому запрос должен быть корректным в общем случае. PostgreSQL, MySQL, ClickHouse и BigQuery доступны в отдельных браузерных песочницах. Бесплатный тариф без карты включает 292 задачи на всех четырёх диалектах. Для последовательного изучения темы можно пройти трек «SQL с нуля → Аналитик»: он связывает базовые запросы с задачами, которые постепенно требуют более сложной агрегации.
- EasyбесплатноКогда оживает Котомаркетbigquery
- EasyбесплатноUnrecognized namebigquery
- EasyбесплатноНабор товаров каждого пользователяclickhouse
- EasyбесплатноЧисло уникальных товаров на пользователяclickhouse
- MediumPremiumГород в карточке пользователяbigquery
- MediumPremiumАудитория каждой платформыbigquery
- MediumPremiumИстория действий каждого пользователяbigquery
- MediumPremiumМесячный ритм выручкиbigquery
Справочник по теме
Подробные статьи с синтаксисом и примерами в справочнике SQL Arena.
Частые вопросы
Что такое GROUP BY SQL?
GROUP BY объединяет строки с одинаковыми значениями указанных столбцов в группы. Обычно его используют вместе с агрегатными функциями COUNT, SUM, AVG, MIN и MAX, чтобы получить по одной итоговой строке на каждую группу.
Как использовать GROUP BY в SQL?
В GROUP BY перечисляют столбцы, по которым нужно сгруппировать результат. Если столбец выводится в SELECT и не находится внутри агрегатной функции, обычно он должен участвовать в группировке.
Что такое HAVING SQL?
HAVING задаёт условие для уже сформированных групп. Поэтому в HAVING можно проверять результаты агрегатных функций, например оставить только группы, для которых COUNT(*) больше заданного значения.
Чем HAVING отличается от WHERE в SQL?
WHERE фильтрует отдельные строки до группировки, а HAVING — группы после GROUP BY. Если условие относится к исходным строкам, обычно нужен WHERE; если к COUNT, SUM или другому агрегату по группе — HAVING.
Можно ли использовать HAVING без GROUP BY?
Во многих СУБД можно: тогда весь результат рассматривается как одна группа. Но типичный сценарий HAVING связан именно с GROUP BY и фильтрацией нескольких сформированных групп.
Практика GROUP BY SQL на задачах
В SQL Arena можно практиковать агрегаты на разных схемах и проверять запросы автоматически. Для анализа доступны ER-диаграммы и EXPLAIN, а режим «Собеседование» запускает сессию с серверным таймером и без подсказок, после которой формируется отчёт; бесплатно он доступен один раз в день.
Решать задачи на GROUP BY