GROUP BY в SQL — полный гайд по группировке данных

Коротко

GROUP BY группирует строки с одинаковыми значениями в указанном столбце и позволяет применить к каждой группе агрегатную функцию — COUNT, SUM, AVG, MIN, MAX. Без GROUP BY невозможно посчитать выручку по месяцам, количество пользователей по городам или конверсию по каналам. На собеседованиях аналитика данных GROUP BY встречается практически в каждой SQL-задаче.

Синтаксис

SELECT column, aggregate_function(column2)
FROM TABLE
WHERE условие
GROUP BY column
ORDER BY ...

GROUP BY стоит после WHERE и до ORDER BY. Сначала WHERE фильтрует строки, затем GROUP BY группирует оставшиеся, а ORDER BY сортирует результат.

Простейший пример — количество заказов по категориям:

SELECT category, COUNT(*) AS order_count
FROM orders
GROUP BY category

Каждое уникальное значение category становится отдельной группой, и COUNT(*) считает количество строк в каждой.

GROUP BY с агрегатными функциями

GROUP BY раскрывается через агрегатные функции. Без них группировка бессмысленна.

COUNT — количество в группе

SELECT city, COUNT(DISTINCT user_id) AS users
FROM users
GROUP BY city
ORDER BY users DESC

COUNT(*) считает все строки в группе, COUNT(DISTINCT column) — уникальные значения. Разница критична: подробнее в статье COUNT(*) vs COUNT(column).

SUM — сумма по группам

SELECT
    product_id,
    SUM(quantity) AS total_sold,
    SUM(amount) AS revenue
FROM order_items
GROUP BY product_id
ORDER BY revenue DESC

AVG — среднее по группам

SELECT
    department,
    ROUND(AVG(salary), 2) AS avg_salary,
    COUNT(*) AS headcount
FROM employees
GROUP BY department

AVG игнорирует NULL — среднее считается только по заполненным значениям. Если нужно учитывать пропуски как нули, используйте AVG(COALESCE(salary, 0)).

GROUP BY по нескольким столбцам

Группировать можно по двум и более столбцам — каждая уникальная комбинация значений станет отдельной группой.

SELECT
    city,
    segment,
    COUNT(*) AS user_count,
    SUM(total_spent) AS revenue
FROM users
GROUP BY city, segment
ORDER BY city, revenue DESC

Если в таблице 5 городов и 3 сегмента, максимум будет 15 групп (5 x 3). На практике меньше, если не все комбинации встречаются.

HAVING — фильтрация групп

WHERE фильтрует строки до группировки. HAVING фильтрует группы после группировки — здесь можно использовать агрегатные функции.

SELECT
    user_id,
    COUNT(*) AS order_count,
    SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5
ORDER BY total_spent DESC

Писать WHERE COUNT(*) > 5 — ошибка: группировка ещё не произошла. Нужно HAVING COUNT(*) > 5. Детальное сравнение — в статье WHERE vs HAVING.

WHERE и HAVING можно комбинировать:

SELECT category, SUM(amount) AS revenue
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY category
HAVING SUM(amount) > 100000

WHERE оставляет заказы с 2025 года, GROUP BY группирует по категориям, HAVING убирает категории с выручкой ниже 100 000.

GROUP BY + ORDER BY

ORDER BY сортирует итоговый результат. Сортировать можно по столбцам из GROUP BY, по агрегатам или по алиасам.

SELECT
    category,
    COUNT(*) AS order_count,
    SUM(amount) AS revenue
FROM orders
GROUP BY category
ORDER BY revenue DESC

ORDER BY выполняется после SELECT, поэтому алиас revenue здесь работает. GROUP BY выполняется до SELECT — в стандартном SQL алиасы в GROUP BY недопустимы (MySQL разрешает, PostgreSQL — нет).

GROUP BY с выражениями

Группировать можно не только по столбцам, но и по выражениям. Самый частый случай — группировка по дате через DATE_TRUNC.

Выручка по месяцам

SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(amount) AS revenue,
    COUNT(DISTINCT user_id) AS buyers
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month

Выражение в GROUP BY должно точно совпадать с выражением в SELECT. Написать GROUP BY month (по алиасу) в PostgreSQL нельзя — нужно повторить DATE_TRUNC('month', order_date).

Пользователи по дню регистрации

SELECT
    DATE_TRUNC('day', created_at) AS reg_day,
    COUNT(*) AS new_users
FROM users
GROUP BY DATE_TRUNC('day', created_at)
ORDER BY reg_day

Практические примеры

Конверсия по каналу привлечения

SELECT
    utm_source,
    COUNT(DISTINCT user_id) AS visitors,
    COUNT(DISTINCT CASE WHEN has_purchase THEN user_id END) AS buyers,
    ROUND(
        COUNT(DISTINCT CASE WHEN has_purchase THEN user_id END)::numeric
        / NULLIF(COUNT(DISTINCT user_id), 0) * 100, 2
    ) AS conversion_pct
FROM user_visits
GROUP BY utm_source
ORDER BY conversion_pct DESC

Выручка по месяцам с накоплением

SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(amount) AS monthly_revenue,
    COUNT(*) AS orders
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month

Средний чек по городам (топ-10)

SELECT
    u.city,
    ROUND(AVG(o.amount), 2) AS avg_check,
    COUNT(*) AS order_count
FROM orders o
JOIN users u ON o.user_id = u.id
GROUP BY u.city
HAVING COUNT(*) >= 30
ORDER BY avg_check DESC
LIMIT 10

HAVING COUNT(*) >= 30 отсекает города с малым количеством заказов, чтобы среднее было статистически осмысленным.

GROUP BY vs PARTITION BY

GROUP BY сворачивает группу строк в одну строку. PARTITION BY в оконных функциях вычисляет агрегат для каждой строки, не сворачивая результат.

-- GROUP BY: одна строка на категорию
SELECT category, SUM(amount) AS revenue
FROM orders
GROUP BY category

-- PARTITION BY: все строки сохранены, revenue добавлен
SELECT
    order_id, category, amount,
    SUM(amount) OVER (PARTITION BY category) AS category_revenue
FROM orders

Подробное сравнение с примерами — в статье GROUP BY vs PARTITION BY.

Типичные ошибки

Столбец в SELECT, но не в GROUP BY и не в агрегате. SELECT user_id, name, COUNT(*) FROM orders GROUP BY user_id — ошибка. Столбец name нужно либо добавить в GROUP BY, либо обернуть в агрегат (например, MAX(name)). PostgreSQL и стандартный SQL это запрещают; MySQL в режиме по умолчанию допускает, но результат непредсказуем.

Агрегат в WHERE вместо HAVING. WHERE SUM(amount) > 10000 — синтаксическая ошибка. WHERE выполняется до GROUP BY, агрегаты на этом этапе недоступны. Нужно HAVING SUM(amount) > 10000.

COUNT(*) вместо COUNT(DISTINCT). После JOIN строки могут дублироваться. Если считаете уникальных пользователей — используйте COUNT(DISTINCT user_id), а не COUNT(*).

Группировка по алиасу в PostgreSQL. SELECT DATE_TRUNC('month', dt) AS month ... GROUP BY month — ошибка в PostgreSQL. Нужно повторить выражение: GROUP BY DATE_TRUNC('month', dt). Или использовать GROUP BY 1 (по порядковому номеру столбца в SELECT).

Вопросы с собеседований

-- Что делает GROUP BY? -- GROUP BY группирует строки с одинаковыми значениями в указанных столбцах. К каждой группе можно применить агрегатную функцию (COUNT, SUM, AVG, MIN, MAX). Результат — одна строка на группу.

-- Какие столбцы можно указывать в SELECT при GROUP BY? -- Только столбцы, перечисленные в GROUP BY, и агрегатные функции. Если столбец не входит в GROUP BY и не обёрнут в агрегат — это ошибка (в стандартном SQL и PostgreSQL).

-- Чем GROUP BY отличается от PARTITION BY? -- GROUP BY сворачивает группу в одну строку. PARTITION BY в оконных функциях вычисляет значение для каждой строки, не сворачивая результат. GROUP BY уменьшает количество строк, PARTITION BY — нет.

-- Напишите запрос: выручка по месяцам за 2025 год. -- SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS revenue FROM orders WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01' GROUP BY DATE_TRUNC('month', order_date) ORDER BY month. WHERE фильтрует строки по дате, GROUP BY группирует по месяцам.

-- Можно ли группировать по выражению? -- Да. GROUP BY принимает выражения: DATE_TRUNC('day', dt), EXTRACT(YEAR FROM dt), CASE WHEN ... END. Выражение в GROUP BY должно совпадать с выражением в SELECT.


Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.

FAQ

В каком порядке выполняются GROUP BY, WHERE, HAVING?

FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. WHERE фильтрует строки до группировки, GROUP BY формирует группы, HAVING фильтрует группы по агрегату. Это объясняет, почему агрегат нельзя использовать в WHERE — на том этапе группировка ещё не произошла.

Можно ли использовать GROUP BY без агрегатных функций?

Формально — да. GROUP BY без агрегатов работает как SELECT DISTINCT: возвращает уникальные комбинации значений. Но на практике GROUP BY всегда идёт с агрегатной функцией — иначе проще написать DISTINCT.

Как GROUP BY обрабатывает NULL?

NULL считается отдельной группой. Все строки с NULL в столбце группировки попадут в одну группу. Это стандартное поведение SQL.

Как тренироваться

GROUP BY — основа SQL для аналитика. Каждый запрос с метриками использует группировку. В тренажёре Карьерник есть задачи на GROUP BY, HAVING, агрегатные функции и условную агрегацию — с разборами. Больше вопросов по всем темам — в разделе с примерами.