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;

Порядок выполнения запроса

Неочевидная вещь: физический порядок выполнения отличается от записи. Вот реальный порядок:

  1. FROM + JOIN
  2. WHERE
  3. GROUP BY
  4. HAVING
  5. SELECT
  6. DISTINCT
  7. ORDER BY
  8. 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. Это показывает, что вы понимаете механику, а не просто зазубрили синтаксис.

Читайте также

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 для иерархий.