GROUP BY SQL: шпаргалка для собеседования
Зачем аналитику GROUP BY
GROUP BY — основа почти любого аналитического запроса. Если не уверены в нём — про оконные функции и сложные JOIN можно даже не начинать.
GROUP BY делает то, ради чего вообще существует SQL в аналитике: сворачивает сырые строки в агрегаты. Сумма по клиенту, среднее по дню, count по сегменту — всё идёт через GROUP BY. На собеседовании его спрашивают в каждом втором вопросе, иногда явно, иногда через ловушки.
Если готовитесь к собеседованию по SQL, этот раздел нужно довести до рефлексов.
Базовый синтаксис
SELECT
столбец_группировки,
агрегатная_функция(столбец)
FROM таблица
WHERE фильтр_до_группировки
GROUP BY столбец_группировки
HAVING фильтр_после_группировки
ORDER BY столбец_или_агрегат- WHERE отсекает строки до группировки.
- GROUP BY собирает строки в группы по значениям указанных столбцов.
- HAVING фильтрует уже посчитанные группы — может использовать агрегаты.
- ORDER BY сортирует результат.
Правило «все столбцы SELECT — в GROUP BY или в агрегате»
Самая частая ошибка новичков:
-- ❌ Ошибка: user_id не в GROUP BY
SELECT user_id, city, SUM(amount)
FROM orders
GROUP BY city;Правильно:
-- ✅ user_id либо убираем, либо добавляем в GROUP BY
SELECT city, SUM(amount)
FROM orders
GROUP BY city;На собеседовании спрашивают: «Можно ли в SELECT указать столбец, которого нет в GROUP BY?» Ответ: нет, если только он не внутри агрегатной функции. Исключение — PostgreSQL и MySQL в нестрогом режиме разрешают, но полагаться на это опасно.
Агрегатные функции
| Функция | Что считает |
|---|---|
COUNT(*) |
Количество строк, включая NULL |
COUNT(col) |
Количество не-NULL значений |
COUNT(DISTINCT col) |
Количество уникальных не-NULL значений |
SUM(col) |
Сумма, NULL игнорирует |
AVG(col) |
Среднее, NULL игнорирует |
MIN(col) / MAX(col) |
Минимум/максимум |
STRING_AGG(col, ',') |
Склейка строк через разделитель |
ARRAY_AGG(col) |
Собирает значения в массив |
Классический вопрос: «Чем отличаются COUNT(*) и COUNT(column)?» Ответ: COUNT(*) считает все строки группы, COUNT(column) — только те, где column не NULL. Если в колонке есть NULL — значения будут разные.
Попробовать силы на подобных вопросах проще всего в тренажёре Карьерник — прямо в Telegram, без регистрации через сайт.
WHERE vs HAVING
Самая популярная ловушка:
-- WHERE работает до группировки — на сырых строках
SELECT city, COUNT(*) AS users
FROM users
WHERE created_at >= '2026-01-01'
GROUP BY city;
-- HAVING работает после — на агрегатах
SELECT city, COUNT(*) AS users
FROM users
GROUP BY city
HAVING COUNT(*) > 100;Запомните: WHERE COUNT(*) > 100 — синтаксическая ошибка. Агрегат в WHERE использовать нельзя.
Если нужно отфильтровать и по строкам, и по агрегатам — используем оба:
SELECT city, COUNT(*) AS users
FROM users
WHERE created_at >= '2026-01-01'
GROUP BY city
HAVING COUNT(*) > 100;Порядок выполнения запроса
Неочевидная вещь: физический порядок выполнения отличается от записи. Вот реальный порядок:
- FROM + JOIN
- WHERE
- GROUP BY
- HAVING
- SELECT
- DISTINCT
- ORDER BY
- LIMIT
Отсюда правило: в HAVING и SELECT можно ссылаться на агрегаты, а в WHERE — нельзя. Алиасы из SELECT в WHERE и GROUP BY использовать тоже нельзя (в большинстве СУБД), потому что SELECT ещё не выполнился.
GROUP BY по нескольким столбцам
SELECT city, device, COUNT(*) AS sessions
FROM events
GROUP BY city, device;Группы создаются по всем уникальным комбинациям значений. Если городов 100 и устройств 3 — в итоге до 300 строк.
На собеседовании спрашивают: «Сколько строк вернёт GROUP BY по трём столбцам?» Ответ: столько, сколько уникальных комбинаций значений этих трёх столбцов в данных — не больше произведения мощностей.
NULL в GROUP BY
NULL — отдельная группа. Все NULL попадают в одну корзину:
SELECT category, COUNT(*) FROM products GROUP BY category;
-- Возвращает строку с category = NULL, если такие естьЭто логически не совсем корректно (NULL ≠ NULL по стандарту), но практически удобно.
GROUPING SETS, ROLLUP, CUBE
Продвинутая часть, но её спрашивают на middle+ позициях.
GROUPING SETS
Несколько уровней группировки в одном запросе:
SELECT city, device, COUNT(*)
FROM events
GROUP BY GROUPING SETS (
(city, device), -- по городу и устройству
(city), -- только по городу
(device), -- только по устройству
() -- общий итог
);Одним запросом получаем четыре уровня агрегации — без четырёх отдельных SELECT + UNION.
ROLLUP
Иерархические итоги от детального к общему:
SELECT year, quarter, month, SUM(revenue)
FROM sales
GROUP BY ROLLUP (year, quarter, month);Вернёт: по месяцу, по кварталу, по году, общий итог.
CUBE
Все возможные комбинации группировок:
SELECT city, device, SUM(revenue)
FROM sales
GROUP BY CUBE (city, device);Это GROUPING SETS со всеми сочетаниями: (city, device), (city), (device), (). Полезно для OLAP-отчётов.
Пройти 30–50 задач по теме за вечер можно в Telegram-тренажёре. Это то, что отличает «знаю» от «уверенно отвечу на собесе».
GROUP BY с оконной функцией
Да, их можно комбинировать в одном запросе:
SELECT
city,
SUM(amount) AS city_revenue,
ROUND(100.0 * SUM(amount) / SUM(SUM(amount)) OVER (), 2) AS pct_of_total
FROM orders
GROUP BY city;SUM(SUM(amount)) OVER () — суммирует все city_revenue, даёт общий итог для расчёта доли. Частая задача: «выведите долю каждого города в общей выручке».
12 задач на GROUP BY с собеседований
Задача 1. Количество заказов по каждому пользователю
SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id;Задача 2. Средний чек по городам, только города с >10 заказами
SELECT city, AVG(amount) AS avg_check
FROM orders
GROUP BY city
HAVING COUNT(*) > 10;Задача 3. Топ-5 пользователей по выручке
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
ORDER BY total DESC
LIMIT 5;Задача 4. Количество уникальных покупателей по дням
SELECT created_at::DATE AS day, COUNT(DISTINCT user_id) AS dau
FROM orders
GROUP BY created_at::DATE;Задача 5. Категории с более чем 1 млн выручки
SELECT category, SUM(amount) AS revenue
FROM orders JOIN products USING (product_id)
GROUP BY category
HAVING SUM(amount) > 1000000;Задача 6. Пользователи с первым заказом в марте 2026
SELECT user_id, MIN(created_at) AS first_order
FROM orders
GROUP BY user_id
HAVING MIN(created_at) >= '2026-03-01'
AND MIN(created_at) < '2026-04-01';Задача 7. Процент заказов в каждой категории
SELECT
category,
COUNT(*) AS orders,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS pct
FROM orders JOIN products USING (product_id)
GROUP BY category;Задача 8. Среднее, медиана и максимум заказа по городу
SELECT city,
AVG(amount) AS avg_amount,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median,
MAX(amount) AS max_amount
FROM orders
GROUP BY city;PERCENTILE_CONT — стандартный способ посчитать медиану в PostgreSQL.
Задача 9. Самый популярный товар в каждой категории
WITH sales AS (
SELECT category, product_id, SUM(quantity) AS sold
FROM order_items JOIN products USING (product_id)
GROUP BY category, product_id
)
SELECT DISTINCT ON (category) category, product_id, sold
FROM sales
ORDER BY category, sold DESC;Задача 10. Клиенты, которые купили все 3 продукта А, B, C
SELECT user_id
FROM orders
WHERE product_id IN ('A', 'B', 'C')
GROUP BY user_id
HAVING COUNT(DISTINCT product_id) = 3;Классический приём: COUNT(DISTINCT) в HAVING проверяет покрытие списка.
Задача 11. Количество активных и неактивных пользователей одной строкой
SELECT
COUNT(*) FILTER (WHERE last_active >= CURRENT_DATE - INTERVAL '30 day') AS active,
COUNT(*) FILTER (WHERE last_active < CURRENT_DATE - INTERVAL '30 day') AS inactive
FROM users;FILTER WHERE — способ сделать условные агрегаты без CASE WHEN.
Задача 12. Иерархический отчёт: по году, кварталу, итог
SELECT
EXTRACT(YEAR FROM created_at) AS yr,
EXTRACT(QUARTER FROM created_at) AS qtr,
SUM(amount)
FROM orders
GROUP BY ROLLUP (EXTRACT(YEAR FROM created_at), EXTRACT(QUARTER FROM created_at));ROLLUP добавит строки с подытогами по году и общим итогом.
Как тренироваться
GROUP BY — один из тех навыков, где теория не работает. Нужно писать запросы, ловить ошибки, ломать HAVING, путать WHERE с HAVING — а потом начнёт получаться быстро и без сомнений.
Тренажёр Карьерник содержит вопросы по GROUP BY и агрегатам: от простого COUNT/SUM до ROLLUP и GROUPING SETS, с разбором типичных ошибок на собеседованиях. Можно тренироваться по 10 минут в день в Telegram.
Совет: на собеседовании всегда явно проговаривайте порядок выполнения — FROM → WHERE → GROUP BY → HAVING → SELECT. Это показывает, что вы понимаете механику, а не просто зазубрили синтаксис.
Читайте также
- Оконные функции SQL: шпаргалка
- JOIN SQL: шпаргалка
- Порядок выполнения SQL-запроса
- Подготовка к собеседованию аналитика
FAQ
Можно ли использовать алиас из SELECT в GROUP BY?
В PostgreSQL и MySQL — можно (они понимают алиасы). В ClickHouse и SQL Server — нельзя, там нужно повторить выражение. Безопаснее писать выражение целиком в GROUP BY — это работает везде и сразу видно, что именно группируется.
Чем отличается GROUP BY от DISTINCT?
DISTINCT убирает дубликаты строк, GROUP BY делает то же + позволяет применять агрегатные функции. Если нужны только уникальные значения — пишите DISTINCT, он читаемее. Если нужны агрегаты по группам — GROUP BY. По производительности они обычно эквивалентны — оптимизатор понимает оба одинаково.
Как работает GROUP BY с NULL?
Все NULL попадают в одну группу. Это противоречит стандартной логике SQL (NULL ≠ NULL), но сделано для удобства. Если хотите явно обработать NULL — оберните в COALESCE: GROUP BY COALESCE(category, 'unknown').
Что такое GROUPING SETS и зачем они?
GROUPING SETS позволяют в одном запросе посчитать несколько уровней агрегации: по городу, по устройству, по городу+устройству, общий итог. Это быстрее, чем делать UNION четырёх отдельных SELECT, и чище для отчётов. ROLLUP и CUBE — частные случаи GROUPING SETS для иерархий.