SQL · Postgres

Тема 04 — Агрегация: GROUP BY / HAVING

Свернуть много строк в одно число на группу. Это ровно то место, где ты в диагностике «забывал GROUP BY». Разберёмся так, чтобы забыть было нельзя.

Что заберёшь с урока

Поймёшь золотое правило: любой столбец в SELECT, который не внутри агрегата, обязан быть в GROUP BY. Научишься считать по группам (COUNT, SUM, AVG…), фильтровать группы через HAVING и видеть разницу между WHERE и HAVING.

Главный источник по теме PostgreSQL Docs — Aggregate Functions (первоисточник, короткий раздел туториала) и справочник Aggregate Functions list. Простыми словами — Mode SQL: GROUP BY.

§1 Агрегат сворачивает строки

Обычный SELECT отдаёт строку на строку. Агрегатная функция берёт много строк и возвращает одно значение. Без GROUP BY она сворачивает всю таблицу в одну строку:

ФункцияЧто считаетЗаметка
COUNT(*)сколько строксчитает все строки группы
COUNT(col)сколько не-NULL значений в столбцеNULL не считается
SUM(col)сумматолько числа
AVG(col)среднеечасто оборачивают в ROUND(…, 2)
MIN(col) / MAX(col)минимум / максимумработает и с датами, и с текстом

§2 Золотое правило GROUP BY

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

Postgres ответит примерно так: column "products.category_id" must appear in the GROUP BY clause…. Логика простая: ты просишь одно число (COUNT) и при этом столбец category_id, у которого много разных значений. Postgres не знает, какое показать — пока ты не скажешь «по одному числу на каждую категорию». Добавь GROUP BY category_id — и всё встаёт на места:

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

Проверка: SELECT category_id, COUNT(*) FROM products; — без GROUP BY. Что сделает Postgres?

Верно: смешивать неагрегированный category_id с COUNT(*) без GROUP BY нельзя — Postgres даёт ошибку «must appear in the GROUP BY clause». Либо группируй по category_id, либо убери его из SELECT.

§3 Порядок выполнения и WHERE vs HAVING

Как и в Теме 01, важен реальный порядок. Для агрегаций он такой:

FROM / JOIN  → собрать строки
WHERE       → отфильтровать строки (до группировки)
GROUP BY    → собрать строки в группы
HAVING      → отфильтровать группы (после агрегации)
SELECT      → вычислить агрегаты и столбцы
ORDER BY    → отсортировать результат

Отсюда всё главное про эти два фильтра:

Правило-подсказка: условие на отдельную строку → WHERE. Условие на итог группы → HAVING.

Проверка: где отфильтровать «оставить только группы, где COUNT(*) > 3»?

Верно: агрегат (COUNT(*)) существует только после GROUP BY, поэтому фильтр по нему живёт в HAVING. В WHERE агрегата ещё нет — там фильтруют исходные строки.

§4 Бонус Postgres: FILTER

Иногда в одной строке нужно несколько «условных» подсчётов. В Postgres есть красивый приём — FILTER:

SELECT COUNT(*)                                   AS total,
       COUNT(*) FILTER (WHERE status = 'paid') AS paid
FROM orders;

Это чище, чем городить CASE внутри SUM. Пригодится в упражнении 11.


§5 Практика · 12 упражнений

решено 0 / 12

Пиши запрос, жми «Проверить» (⌘/Ctrl+Enter). Сначала сам — усилие памяти строит навык. Подсказка и решение рядом, бери их только когда реально застрял. Держи схему под рукой. Задачи идут по нарастанию.

1. Разминка (это ты уже умеешь): сколько заказов в каждом статусе? Выведи status и количество.

2. Сколько всего товаров в таблице products? Одно число (без GROUP BY — сворачиваем всю таблицу).

3. В одной строке выведи самую низкую и самую высокую цену товара (два столбца).

4. Сколько товаров в каждой категории? Выведи category_id и количество. (Заметь: категория 7 «Garden» не появится — у неё нет товаров. Как её вернуть — тема JOIN вглубь.)

5. Средняя цена товара в каждой категории, округлённая до 2 знаков. Выведи category_id и среднюю цену.

6. Оставь только категории, где больше 3 товаров. Выведи category_id и количество. Фильтр — по итогу группы!

7. Считай только товары в наличии (stock > 0). Для каждой категории выведи, сколько таких товаров, и оставь только категории, где их 3 или больше. Здесь работают оба фильтра — WHERE и HAVING.

8. В таблице customers одной строкой выведи два числа: сколько всего покупателей и у скольких заполнена страна (country). Числа разные — подумай почему.

9. Выручка по категориям (та самая задача 6 из диагностики — теперь с опорой). Для каждой категории, где были продажи, выведи имя категории (name) и суммарную выручку — сумму quantity * unit_price по проданным товарам.

10. Найди покупателей с 2 и более заказами. Выведи их name и число заказов.

11. Одной строкой по всем заказам посчитай три числа: всего заказов, оплачено (status = 'paid') и отменено (status = 'cancelled'). Используй COUNT(*) FILTER (WHERE …).

12. Капстоун. Только по заказам со статусом paid: для каждого покупателя (name) посчитай его суммарную выручку (сумма quantity * unit_price по позициям его paid-заказов). Оставь только тех, у кого сумма больше 100, и отсортируй по сумме по убыванию. (важен порядок)

Частые ловушки

1) Неагрегированный столбец в SELECT без GROUP BY → ошибка «must appear in the GROUP BY clause». 2) Условие по COUNT/SUM нельзя класть в WHERE — только в HAVING. 3) COUNT(col) молча пропускает NULL; COUNT(*) — нет.