Урок 5 из 8 · 25 мин · обновлено

Группировка: GROUP BY и HAVING

Как посчитать итоги по каждой группе — по статусу, жанру, клиенту или месяцу: GROUP BY, отбор групп через HAVING, порядок выполнения запроса и правило столбцов в SELECT.

Итоги по группам

Агрегаты из прошлого урока считали итог по всей таблице. Но чаще нужен отчёт по категориям: сколько заказов в каждом статусе, сколько книг в каждом жанре, сколько потратил каждый клиент. Для этого есть GROUP BY:

SQL
SELECT status, COUNT(*) AS orders_count FROM orders GROUP BY status;
Результат
status    | orders_count
в пути    | 2
доставлен | 6
отменён   | 1

База разложила девять заказов по группам — по одной на каждый статус — и в каждой посчитала COUNT. Агрегатов может быть несколько:

SQL
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. Выручка по каждому клиенту без отменённых заказов, от самого щедрого:

SQL
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:

SQL
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, полезно знать, в каком порядке база выполняет запрос. Он не совпадает с порядком слов:

  1. FROM — берёт таблицу;
  2. WHERE — отбирает строки;
  3. GROUP BY — собирает их в группы;
  4. HAVING — отбрасывает неподходящие группы;
  5. SELECT — считает и выбирает столбцы;
  6. ORDER BY — сортирует результат;
  7. LIMIT — отрезает нужное количество строк.

Условие на отдельные записи — в WHERE, условие на итог группы — в HAVING.

Правило столбцов в SELECT

В групповом запросе в SELECT можно писать только столбцы из GROUP BY и агрегаты. Посмотрите, что будет, если нарушить правило:

SQL
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

Если в столбце группировки есть пустые значения, они собираются в отдельную группу:

SQL
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.

Решение
SQL
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.

Мини-тест: проверьте себя

Закрепите урок: несколько вопросов, разбор ответов — в конце.

  1. Что сделает GROUP BY status в запросе к заказам?

  2. Чем HAVING отличается от WHERE?

  3. Нужны клиенты, которые потратили больше 1000 рублей. Куда поставить условие SUM(amount) > 1000?

  4. Почему запрос SELECT customer_id, amount, SUM(amount) FROM orders GROUP BY customer_id — плохая идея?