Урок 5 из 8 · 25 мин · обновлено
Группировка: GROUP BY и HAVING
Как посчитать итоги по каждой группе — по статусу, жанру, клиенту или месяцу: GROUP BY, отбор групп через HAVING, порядок выполнения запроса и правило столбцов в SELECT.
Итоги по группам
Агрегаты из прошлого урока считали итог по всей таблице. Но чаще нужен отчёт по категориям: сколько заказов в каждом статусе, сколько книг в каждом жанре, сколько потратил каждый клиент. Для этого есть GROUP BY:
SELECT status, COUNT(*) AS orders_count FROM orders GROUP BY status;
status | orders_count
в пути | 2
доставлен | 6
отменён | 1
База разложила девять заказов по группам — по одной на каждый статус — и в каждой посчитала COUNT. Агрегатов может быть несколько:
SELECT genre, COUNT(*) AS books_count,
MIN(price) AS min_price, MAX(price) AS max_price
FROM books GROUP BY genre;
genre | books_count | min_price | max_price
IT | 1 | 1200 | 1200
наука | 2 | 720 | 890
роман | 3 | 450 | 590
фэнтези | 2 | 540 | 650
Группировка отлично сочетается с WHERE и ORDER BY. Выручка по каждому клиенту без отменённых заказов, от самого щедрого:
SELECT customer_id, COUNT(*) AS orders_count, SUM(amount) AS total
FROM orders
WHERE status <> 'отменён'
GROUP BY customer_id
ORDER BY total DESC;
customer_id | orders_count | total
2 | 2 | 1740
1 | 2 | 1310
3 | 2 | 1130
5 | 1 | 650
4 | 1 | 450
Клиента 6 в отчёте нет: у Егора нет ни одного заказа, а группы собираются только из существующих строк. Как показать и его, разберём в следующем уроке.
HAVING: отбор групп
Оставим только клиентов, которые потратили больше 1000 рублей. WHERE здесь не подойдёт: он работает до группировки, и суммы по клиенту в этот момент ещё нет. Для групп есть HAVING:
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE status <> 'отменён'
GROUP BY customer_id
HAVING SUM(amount) > 1000
ORDER BY total DESC;
customer_id | total
2 | 1740
1 | 1310
3 | 1130
Чтобы не путать WHERE и HAVING, полезно знать, в каком порядке база выполняет запрос. Он не совпадает с порядком слов:
FROM— берёт таблицу;WHERE— отбирает строки;GROUP BY— собирает их в группы;HAVING— отбрасывает неподходящие группы;SELECT— считает и выбирает столбцы;ORDER BY— сортирует результат;LIMIT— отрезает нужное количество строк.
Условие на отдельные записи — в WHERE, условие на итог группы — в HAVING.
Правило столбцов в SELECT
В групповом запросе в SELECT можно писать только столбцы из GROUP BY и агрегаты. Посмотрите, что будет, если нарушить правило:
SELECT customer_id, amount, SUM(amount) FROM orders GROUP BY customer_id;
В группе клиента 2 три заказа с разными суммами, и непонятно, какую из них показать в столбце amount. PostgreSQL на такой запрос ответит ошибкой:
ERROR: column "orders.amount" must appear in the GROUP BY clause
or be used in an aggregate function
SQLite запрос выполнит, но в amount покажет сумму одного случайного заказа из группы. Это хуже ошибки: результат выглядит правдоподобно, но ничего не значит. Если значение нужно, оберните его в агрегат — MIN, MAX, SUM — или добавьте столбец в GROUP BY.
Группа для NULL
Если в столбце группировки есть пустые значения, они собираются в отдельную группу:
SELECT city, COUNT(*) AS customers_count FROM customers GROUP BY city;
city | customers_count
NULL | 1
Казань | 2
Москва | 2
Пермь | 1
Это удобно для проверки данных: строка с NULL сразу показывает, сколько записей без города.
Попробуйте сами. Постройте отчёт по месяцам без отменённых заказов: месяц, количество заказов и выручка. Месяц достаньте из даты функцией substr(order_date, 1, 7) — она берёт 7 символов с начала строки, например 2026-05.
Решение
SELECT substr(order_date, 1, 7) AS month,
COUNT(*) AS orders_count,
SUM(amount) AS revenue
FROM orders
WHERE status <> 'отменён'
GROUP BY month
ORDER BY month;
month | orders_count | revenue
2026-05 | 3 | 2510
2026-06 | 5 | 2770
Группируем по посчитанному значению месяца. SQLite, PostgreSQL и MySQL разрешают сослаться в GROUP BY на имя month из SELECT. Если ваша база так не умеет, повторите выражение целиком: GROUP BY substr(order_date, 1, 7).
Проверка: 2510 + 2770 = 5280 — столько же, сколько выручка без отменённых заказов из урока 4.
Мини-тест: проверьте себя
Закрепите урок: несколько вопросов, разбор ответов — в конце.
-
Что сделает GROUP BY status в запросе к заказам?
Группировка не удаляет и не сортирует строки — она собирает их в группы, а COUNT или SUM считают итог внутри каждой.
-
Чем HAVING отличается от WHERE?
Итог группы появляется только после группировки, поэтому условие на него пишут в HAVING. Условие на отдельные строки — в WHERE.
-
Нужны клиенты, которые потратили больше 1000 рублей. Куда поставить условие SUM(amount) > 1000?
Сумма по клиенту — итог группы. В WHERE её ещё нет: WHERE работает до группировки.
-
Почему запрос SELECT customer_id, amount, SUM(amount) FROM orders GROUP BY customer_id — плохая идея?
PostgreSQL на такой запрос выдаст ошибку, а SQLite покажет сумму одного случайного заказа из группы. В SELECT оставляют столбцы из GROUP BY и агрегаты.