Это часть 4 из 10 учебника «SQL с нуля для аналитика». Содержание серии в конце поста. ← Часть 3
TL;DR: Агрегатные функции — COUNT (сколько), SUM (сумма), AVG (среднее), MIN/MAX (минимум/максимум). GROUP BY группирует строки по колонке, агрегаты считаются внутри каждой группы. HAVING фильтрует группы (как WHERE для агрегатов). 90% метрик аналитика — это GROUP BY + агрегаты.
В этой части:
- 5 главных агрегатных функций
- Как работает GROUP BY (с картинкой потока данных)
- WHERE vs HAVING — когда какое
- Что нельзя писать в SELECT при GROUP BY
- COUNT(*) vs COUNT(col) vs COUNT(DISTINCT)
Какие 5 агрегатных функций должен знать аналитик?
| Функция | Что считает | Пример |
|---|---|---|
COUNT(*) | Количество строк | COUNT(*) AS orders |
COUNT(col) | Количество не-NULL значений | COUNT(email) |
COUNT(DISTINCT col) | Уникальных значений | COUNT(DISTINCT user_id) |
SUM(col) | Сумма | SUM(amount) AS revenue |
AVG(col) | Среднее | AVG(amount) AS avg_check |
MIN(col) / MAX(col) | Минимум / Максимум | MAX(created_at) |
Примеры:
-- Сколько у нас пользователей всего
SELECT COUNT(*) FROM users;
-- Сколько уникальных юзеров делали заказы
SELECT COUNT(DISTINCT user_id) FROM orders;
-- Общая выручка
SELECT SUM(amount) AS total_revenue FROM orders WHERE status = 'paid';
Как работает GROUP BY?
GROUP BY группирует строки по значению колонки. Внутри каждой группы вычисляются агрегаты.
!GROUP BY flow: строки группируются по колонке и схлопываются в агрегат
Анимация — следи как строки переходят в свои группы:
!GROUP BY animated: строки оседают по странам
Идея:
Таблица orders:
user_id | amount
1 | 100
1 | 200
2 | 500
3 | 50
После GROUP BY user_id:
Группа user_id=1: [100, 200]
Группа user_id=2: [500]
Группа user_id=3: [50]
После SUM(amount):
user_id=1: 300
user_id=2: 500
user_id=3: 50
В SQL:
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id;
Например, первая метрика большинства аналитиков: «выручка по дням». SELECT DATE(created_at), SUM(amount) FROM orders GROUP BY 1 ORDER BY 1. Откроет глаза на seasonality, day-of-week-эффект, аномалии.
Что нельзя писать в SELECT при GROUP BY?
Правило: каждая колонка в SELECT должна быть либо в GROUP BY, либо внутри агрегата.
-- ОШИБКА: email не в GROUP BY и не агрегат
SELECT user_id, email, SUM(amount)
FROM orders o
JOIN users u ON o.user_id = u.id
GROUP BY user_id;
-- ПРАВИЛЬНО: добавить email в GROUP BY
SELECT user_id, email, SUM(amount)
FROM orders o
JOIN users u ON o.user_id = u.id
GROUP BY user_id, email;
-- ИЛИ обернуть в агрегат (для одинаковых значений)
SELECT user_id, MAX(email), SUM(amount)
FROM orders o
JOIN users u ON o.user_id = u.id
GROUP BY user_id;
(JOIN разберём в Части 5).
Чем HAVING отличается от WHERE?
WHEREфильтрует строки до агрегацииHAVINGфильтрует группы после агрегации
Пример:
-- Все юзеры с выручкой > 10000
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE status = 'paid' -- фильтр строк до GROUP BY
GROUP BY user_id
HAVING SUM(amount) > 10000; -- фильтр групп после
| Что | WHERE | HAVING |
|---|---|---|
| Когда работает | До GROUP BY | После GROUP BY |
| Можно использовать агрегаты | ❌ | ✅ |
| Можно использовать колонки таблицы | ✅ | Только из GROUP BY |
Правило: WHERE по возможности, HAVING — только для условий на агрегаты. WHERE быстрее, потому что меньше данных идёт в GROUP BY.
Чем COUNT(*) отличается от COUNT(col)?
COUNT(*)— все строки (включая с NULL)COUNT(col)— только строки где col не NULLCOUNT(DISTINCT col)— уникальные не-NULL значения
-- Таблица users (10 строк, у 3 email IS NULL)
SELECT
COUNT(*), -- 10
COUNT(email), -- 7 (без NULL)
COUNT(DISTINCT email) -- например 6 (если 1 дубль)
FROM users;
Подробнее про NULL — в Части 7.
Какие 6 типичных задач аналитика через GROUP BY?
- Выручка по странам:
SUM(amount)GROUP BYcountry - Уникальные юзеры в день:
COUNT(DISTINCT user_id)GROUP BYDATE(event_ts) - Средний чек по категориям:
AVG(amount)GROUP BYcategory - Топ-10 продуктов:
COUNT(*)GROUP BYproduct_idORDER BYCOUNT(*) DESCLIMIT 10 - Когда последний заказ юзера:
MAX(created_at)GROUP BYuser_id - Конверсия по каналам:
COUNT(CASE WHEN paid = TRUE THEN 1 END) * 100.0 / COUNT(*)GROUP BYchannel
(CASE WHEN разберём в Части 7).
Типичные топ-5 запросов: (1) выручка/день, (2) DAU, (3) средний чек по категориям, (4) топ-10 продуктов, (5) конверсия по каналам.
Частые вопросы про GROUP BY и агрегаты
Можно ли в GROUP BY использовать индексы колонок?
GROUP BY 1, 2 (по индексу колонки в SELECT). Работает, но плохо читается. Лучше явно GROUP BY country, region.
Что делать если нужно агрегат внутри агрегата?
Подзапросы или CTE — разбираем в Части 6. Пример: «средняя выручка на пользователя» — сначала сумма по юзеру, потом среднее.
COUNT(1) — это то же что COUNT(*)?
Да. COUNT(1), COUNT(*), COUNT('any literal') — все считают строки. COUNT(*) — каноничный.
Почему GROUP BY медленный?
Требует сортировки или hash table. На больших данных — bottleneck. Оптимизация: партиционирование, индексы, materialized views. См. гайд по PG индексам.
Можно ли SUM по нескольким колонкам?
SUM(col1 + col2) — работает (предварительно сложит). SUM(col1) + SUM(col2) — тоже работает (сложит сумму). Эквивалентны при отсутствии NULL.
Что дальше?
В Части 5 — JOIN: как соединять таблицы. Без JOIN ты ограничен одной таблицей, а реальный аналитик работает с 5-20 таблицами в одном запросе.
Сейчас открой SQL-тренажёр и попробуй посчитать «выручку по странам через GROUP BY + SUM». Первые 5 задач бесплатны.
В Pro — безлимит мок-собесов на AI-интервью + 491 SQL-задача + 612 тестовых заданий + 50+ блог-постов.
Навигация по учебнику
← Часть 3 | Часть 4: GROUP BY и агрегаты | Часть 5 →
Содержание серии: 1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9 · 10
См. также GROUP BY и HAVING правильно для углубления.
Источники
- PostgreSQL Docs: «Aggregate Functions» (postgresql.org/docs/current/functions-aggregate.html)
- PostgreSQL Docs: «GROUP BY and HAVING» (postgresql.org/docs/current/queries-table-expressions.html)