SQLGROUP BYaggregationtutorial

Что такое GROUP BY в SQL?

GROUP BY — это команда «схлопни строки в группы и посчитай каждую». Простыми словами: как получить «сколько заказов у каждого клиента» одной строкой кода. Что можно и нельзя писать в SELECT, разница с DISTINCT, GROUP BY по нескольким колонкам и частые ошибки.

14 мин чтенияСправочникSQL · GROUP BY · aggregation · tutorial

GROUP BY — это команда, которая превращает много обычных строк в аккуратную сводку по группам.

Без GROUP BY таблица отвечает на вопрос: «Какие строки у нас есть?»

С GROUP BY таблица отвечает на другой вопрос: «Какие итоги получаются по каждой группе?»

Например:

  • сколько заказов сделал каждый клиент;
  • сколько пользователей пришло из каждой страны;
  • какая сумма продаж у каждой категории товаров;
  • какая средняя зарплата в каждом отделе;
  • какие теги в блоге используются чаще всего.

Проще говоря, GROUP BY нужен, когда ты хочешь не просто посмотреть данные, а посчитать статистику.

Простая идея GROUP BY

Представь таблицу заказов. В ней каждая строка — отдельный заказ:

order_id customer_id amount
1 101 500
2 102 300
3 101 700
4 103 200
5 101 100

Если сделать обычный SELECT, ты увидишь все заказы по отдельности.

Но если написать:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id;

то база сгруппирует строки по customer_id.

Результат:

customer_id orders_count
101 3
102 1
103 1

Пять строк превратились в три строки: по одной строке на каждого клиента.

Вот это и есть смысл GROUP BY: собрать одинаковые значения в группы и посчитать что-то внутри каждой группы.

Зачем нужен GROUP BY

GROUP BY почти всегда идёт рядом с агрегатными функциями.

Агрегатная функция — это функция, которая берёт несколько строк и возвращает одно итоговое значение.

Самые частые агрегатные функции:

Функция Что делает
COUNT(*) Считает количество строк
SUM(column) Считает сумму
AVG(column) Считает среднее значение
MIN(column) Находит минимальное значение
MAX(column) Находит максимальное значение

Например, если у нас есть таблица товаров, можно посчитать статистику по каждой категории:

SELECT
  category,
  COUNT(*) AS products_count,
  SUM(price) AS total_price,
  AVG(price) AS avg_price,
  MIN(price) AS min_price,
  MAX(price) AS max_price
FROM products
GROUP BY category;

Такой запрос говорит базе:

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

Результат может быть таким:

category products_count total_price avg_price min_price max_price
books 4 3200 800 500 1200
games 3 7500 2500 1500 3500
courses 2 10000 5000 4000 6000

Вместо списка отдельных товаров мы получили отчёт.

Базовый синтаксис GROUP BY

Самый простой вариант выглядит так:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Разберём по шагам.

База берёт таблицу users, находит все строки, смотрит на значение в колонке country и собирает одинаковые страны вместе.

Если в таблице есть 100 пользователей из RU, 50 пользователей из US и 30 пользователей из DE, то получится три группы.

Потом COUNT(*) считает количество строк внутри каждой группы.

На выходе будет одна строка на каждую страну.

Пример с таблицей users

Есть таблица users:

id name country
1 Anna RU
2 Bob US
3 Vera RU
4 Gleb RU
5 Dan US

Запрос:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Результат:

country users_count
RU 3
US 2

Что произошло:

  • строки с RU попали в одну группу;
  • строки с US попали в другую группу;
  • COUNT(*) посчитал строки внутри каждой группы.

То есть GROUP BY country не показывает каждого пользователя отдельно. Он показывает итог по каждой стране.

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

Группировать можно не только по одной колонке.

Например, у пользователей есть страна и тариф:

id country tier
1 RU free
2 RU paid
3 RU free
4 US free
5 US paid
6 US paid

Можно посчитать пользователей по каждой паре country + tier:

SELECT
  country,
  tier,
  COUNT(*) AS users_count
FROM users
GROUP BY country, tier
ORDER BY country, tier;

Результат:

country tier users_count
RU free 2
RU paid 1
US free 1
US paid 2

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

То есть:

  • RU + free — одна группа;
  • RU + paid — другая группа;
  • US + free — третья группа;
  • US + paid — четвёртая группа.

Простое правило:

Чем больше колонок в GROUP BY, тем подробнее становится группировка.

Главное правило SELECT при GROUP BY

Это самый важный момент.

Когда в запросе есть GROUP BY, в SELECT можно указывать только:

  1. колонки, которые есть в GROUP BY;
  2. агрегатные функции.

Например, так можно:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Почему можно?

Потому что country есть в GROUP BY, а COUNT(*) — агрегатная функция.

А вот так нельзя:

SELECT
  country,
  name,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Почему нельзя?

Потому что в группе RU может быть несколько пользователей: Anna, Vera, Gleb.

Какое имя должна показать база в колонке name?

Одно? Какое именно? Первое? Последнее? Случайное?

SQL не хочет угадывать. Поэтому и появляется ошибка: колонка должна либо участвовать в группировке, либо быть внутри агрегатной функции.

Правильно можно сделать так:

SELECT
  country,
  name,
  COUNT(*) AS rows_count
FROM users
GROUP BY country, name;

Но теперь группировка изменилась. Группа — это уже не страна, а пара country + name.

Или можно так:

SELECT
  country,
  MAX(name) AS any_name,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Но здесь MAX(name) не означает «главное имя». Для текста это просто одно из значений по правилу сортировки. Использовать такой приём стоит только тогда, когда ты понимаешь, зачем тебе это нужно.

Почему нельзя просто вывести любую колонку из группы

Представим группу по стране RU:

id name country
1 Anna RU
3 Vera RU
4 Gleb RU

Если запрос такой:

SELECT
  country,
  name,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

то результат должен быть одной строкой на страну:

country name users_count
RU ? 3

С country всё понятно: вся группа имеет значение RU.

С COUNT(*) тоже всё понятно: в группе 3 строки.

А вот с name непонятно. В группе три разных имени. Поэтому нормальная база данных не даст выполнить такой запрос.

Это не придирка SQL, а защита от странного отчёта.

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

Новичкам часто кажется, что SQL выполняется сверху вниз: сначала SELECT, потом FROM, потом GROUP BY.

На самом деле логический порядок другой.

Упрощённо запрос выполняется так:

  1. FROM — откуда берём строки.
  2. WHERE — какие строки оставляем до группировки.
  3. GROUP BY — как собираем строки в группы.
  4. HAVING — какие группы оставляем после группировки.
  5. SELECT — что выводим в результате.
  6. ORDER BY — как сортируем результат.
  7. LIMIT — сколько строк оставляем.

Например:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE registered_at >= DATE '2026-01-01'
GROUP BY country
HAVING COUNT(*) >= 10
ORDER BY users_count DESC
LIMIT 5;

Что здесь происходит:

  • сначала берём пользователей из users;
  • оставляем только тех, кто зарегистрировался с 2026-01-01;
  • группируем по стране;
  • оставляем только страны, где пользователей минимум 10;
  • выводим страну и количество пользователей;
  • сортируем по количеству;
  • берём первые 5 строк.

Это уже полноценный маленький отчёт.

WHERE и HAVING: в чём разница

WHERE и HAVING оба фильтруют данные, но делают это в разные моменты.

WHERE фильтрует строки до группировки.

HAVING фильтрует группы после группировки.

Например, нужно посчитать пользователей по странам, но брать только активных пользователей.

SELECT
  country,
  COUNT(*) AS active_users_count
FROM users
WHERE status = 'active'
GROUP BY country;

Здесь WHERE status = 'active' сначала убрал неактивных пользователей, и только потом база сгруппировала оставшиеся строки по странам.

А теперь другая задача: показать только те страны, где активных пользователей больше 100.

SELECT
  country,
  COUNT(*) AS active_users_count
FROM users
WHERE status = 'active'
GROUP BY country
HAVING COUNT(*) > 100;

Здесь:

  • WHERE оставляет только активных пользователей;
  • GROUP BY собирает их по странам;
  • HAVING оставляет только те группы, где COUNT(*) > 100.

Плохой вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE COUNT(*) > 100
GROUP BY country;

Так нельзя, потому что на этапе WHERE групп ещё нет. Значит, COUNT(*) ещё не посчитан.

Запомнить можно так:

WHERE — фильтр для строк.
HAVING — фильтр для групп.

GROUP BY и ORDER BY

После группировки результат можно отсортировать.

Например, найти топ-5 стран по количеству пользователей:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
ORDER BY users_count DESC
LIMIT 5;

Здесь ORDER BY users_count DESC сортирует уже готовые группы: сначала страны с большим количеством пользователей, потом с меньшим.

Можно сортировать и прямо по агрегатной функции:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
ORDER BY COUNT(*) DESC;

Но чаще удобнее дать агрегату понятное имя через AS, а потом сортировать по этому имени.

GROUP BY и DISTINCT

GROUP BY иногда путают с DISTINCT, потому что оба могут убрать повторяющиеся значения.

Но задачи у них разные.

DISTINCT нужен, когда ты хочешь получить список уникальных значений.

SELECT DISTINCT country
FROM users;

Результат:

country
RU
US
DE

А GROUP BY нужен, когда ты хочешь не просто уникальные значения, а ещё и расчёты по каждой группе.

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Результат:

country users_count
RU 100
US 50
DE 30

Простая разница:

  • DISTINCT отвечает: «Какие разные значения есть?»
  • GROUP BY отвечает: «Какие группы есть и какие итоги у каждой группы?»

Если нужен только список стран — бери DISTINCT.

Если нужно количество пользователей по странам — бери GROUP BY.

COUNT(*) и COUNT(column)

Это важная тема, потому что новички часто думают, что COUNT(*) и COUNT(email) делают одно и то же.

На самом деле нет.

COUNT(*) считает все строки в группе.

COUNT(column) считает только строки, где в этой колонке не NULL.

Пример:

id country email
1 RU a@example.com
2 RU NULL
3 RU c@example.com
4 US NULL
5 US e@example.com

Запрос:

SELECT
  country,
  COUNT(*) AS rows_count,
  COUNT(email) AS emails_count
FROM users
GROUP BY country;

Результат:

country rows_count emails_count
RU 3 2
US 2 1

Для RU всего 3 строки, но email есть только у 2 пользователей.

Поэтому:

  • если нужно посчитать строки — используй COUNT(*);
  • если нужно посчитать заполненные значения в колонке — используй COUNT(column).

GROUP BY и NULL

NULL в GROUP BY ведёт себя особым образом: все строки с NULL в группируемой колонке попадают в одну группу.

Например:

id country
1 RU
2 NULL
3 US
4 NULL
5 RU

Запрос:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
ORDER BY country;

Результат может быть таким:

country users_count
RU 2
US 1
NULL 2

То есть NULL не исчезает сам по себе. Он образует отдельную группу.

Если не хочешь видеть пользователей без страны, добавь фильтр до группировки:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE country IS NOT NULL
GROUP BY country
ORDER BY country;

GROUP BY по датам

С датами нужно быть аккуратным.

Если сгруппировать по полной дате и времени, можно получить слишком много групп.

Например:

SELECT
  created_at,
  COUNT(*) AS orders_count
FROM orders
GROUP BY created_at;

Если created_at содержит точное время до секунд, почти каждый заказ может оказаться в отдельной группе.

Для отчётов чаще группируют не по полной дате, а по периоду: день, месяц, год.

Например, заказы по дням:

SELECT
  DATE_TRUNC('day', created_at) AS day,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

Заказы по месяцам:

SELECT
  DATE_TRUNC('month', created_at) AS month,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

DATE_TRUNC('month', created_at) обрезает дату до начала месяца.

Например:

created_at month
2026-03-15 10:30:00 2026-03-01 00:00:00
2026-03-22 18:10:00 2026-03-01 00:00:00
2026-04-02 09:00:00 2026-04-01 00:00:00

Так мартовские даты попадут в одну группу, апрельские — в другую.

GROUP BY по месяцу: частая ловушка

Иногда хочется написать так:

SELECT
  EXTRACT(MONTH FROM created_at) AS month,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

Такой запрос сгруппирует заказы по номеру месяца: 1, 2, 3 и так далее.

Но есть проблема: январь 2025 года и январь 2026 года попадут в одну группу.

Если данные только за один год — всё нормально. Если данных несколько лет — отчёт станет неправильным.

Лучше добавить год:

SELECT
  EXTRACT(YEAR FROM created_at) AS year,
  EXTRACT(MONTH FROM created_at) AS month,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;

Или использовать DATE_TRUNC:

SELECT
  DATE_TRUNC('month', created_at) AS month,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

Для отчётов по времени DATE_TRUNC часто удобнее и безопаснее.

GROUP BY и SUM

Один из самых частых сценариев — сумма по группам.

Например, нужно посчитать выручку по каждому клиенту:

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
ORDER BY total_amount DESC;

Результат:

customer_id total_amount
101 1300
102 800
103 200

Так можно быстро понять, какие клиенты принесли больше всего денег.

GROUP BY и AVG

AVG считает среднее значение внутри группы.

Например, средний чек по каждому клиенту:

SELECT
  customer_id,
  AVG(amount) AS avg_order_amount
FROM orders
GROUP BY customer_id
ORDER BY avg_order_amount DESC;

Если у клиента было три заказа на 500, 700 и 100, средний чек будет:

SELECT (500 + 700 + 100) / 3.0 AS avg_amount;

Результат:

avg_amount
433.3333333333333333

В реальных отчётах среднее часто округляют:

SELECT
  customer_id,
  ROUND(AVG(amount), 2) AS avg_order_amount
FROM orders
GROUP BY customer_id
ORDER BY avg_order_amount DESC;

GROUP BY и MIN/MAX

MIN и MAX помогают найти минимальное и максимальное значение внутри каждой группы.

Например, первый и последний заказ клиента:

SELECT
  customer_id,
  MIN(created_at) AS first_order_at,
  MAX(created_at) AS last_order_at
FROM orders
GROUP BY customer_id;

Результат:

customer_id first_order_at last_order_at
101 2026-01-10 12:00:00 2026-03-05 09:30:00
102 2026-02-01 15:20:00 2026-02-18 11:10:00

Такой запрос часто используют, чтобы понять:

  • когда пользователь впервые совершил действие;
  • когда он был активен в последний раз;
  • какой минимальный или максимальный показатель есть в группе.

Полезный пример: отчёт по заказам клиентов

Допустим, есть таблица orders:

id customer_id created_at amount
1 101 2026-01-10 12:00:00 500
2 102 2026-01-12 15:00:00 300
3 101 2026-02-03 10:00:00 700
4 103 2026-02-20 09:30:00 200
5 101 2026-03-01 18:00:00 100

Можно собрать нормальный отчёт по каждому клиенту:

SELECT
  customer_id,
  COUNT(*) AS orders_count,
  SUM(amount) AS total_amount,
  ROUND(AVG(amount), 2) AS avg_amount,
  MIN(created_at) AS first_order_at,
  MAX(created_at) AS last_order_at
FROM orders
GROUP BY customer_id
ORDER BY total_amount DESC;

Результат:

customer_id orders_count total_amount avg_amount first_order_at last_order_at
101 3 1300 433.33 2026-01-10 12:00:00 2026-03-01 18:00:00
102 1 300 300.00 2026-01-12 15:00:00 2026-01-12 15:00:00
103 1 200 200.00 2026-02-20 09:30:00 2026-02-20 09:30:00

Вот в этом месте GROUP BY начинает ощущаться не как странная SQL-команда, а как инструмент аналитики.

У тебя была таблица отдельных заказов. А получился отчёт по клиентам.

Частые ошибки новичков

Ошибка 1. Добавить в SELECT колонку, которой нет в GROUP BY

Плохой вариант:

SELECT
  country,
  name,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Проблема: в одной стране может быть много имён. База не знает, какое имя показать.

Правильный вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

Или, если нужна группировка ещё и по имени:

SELECT
  country,
  name,
  COUNT(*) AS users_count
FROM users
GROUP BY country, name;

Ошибка 2. Фильтровать агрегаты через WHERE

Плохой вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE COUNT(*) > 100
GROUP BY country;

Так нельзя, потому что WHERE работает до группировки.

Правильный вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

HAVING работает после группировки и умеет фильтровать по агрегатам.

Ошибка 3. Путать COUNT(*) и COUNT(column)

SELECT
  country,
  COUNT(*) AS rows_count,
  COUNT(email) AS emails_count
FROM users
GROUP BY country;

COUNT(*) считает все строки.

COUNT(email) считает только строки, где email не NULL.

Если в колонке много пустых значений, результаты будут сильно отличаться.

Ошибка 4. Группировать по слишком точному значению

Например:

SELECT
  created_at,
  COUNT(*) AS orders_count
FROM orders
GROUP BY created_at;

Если в created_at есть часы, минуты и секунды, групп получится слишком много.

Для отчёта по месяцам лучше так:

SELECT
  DATE_TRUNC('month', created_at) AS month,
  COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;

Ошибка 5. Забыть про NULL

Если группировать по колонке, где есть NULL, все NULL попадут в одну отдельную группу.

Если такие строки не нужны, фильтруй их заранее:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE country IS NOT NULL
GROUP BY country;

Ошибка 6. Группировать по месяцу без года

Плохой вариант для данных за несколько лет:

SELECT
  EXTRACT(MONTH FROM created_at) AS month,
  SUM(amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;

Так январь разных лет смешается.

Лучше:

SELECT
  EXTRACT(YEAR FROM created_at) AS year,
  EXTRACT(MONTH FROM created_at) AS month,
  SUM(amount) AS total_amount
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;

Или:

SELECT
  DATE_TRUNC('month', created_at) AS month,
  SUM(amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;

GROUP BY в PostgreSQL и MySQL

В PostgreSQL правило строгое: если колонка стоит в SELECT, она должна быть либо в GROUP BY, либо внутри агрегатной функции.

Например, такой запрос PostgreSQL не пропустит:

SELECT
  country,
  name,
  COUNT(*) AS users_count
FROM users
GROUP BY country;

И это хорошо: база защищает тебя от случайного и непонятного результата.

В MySQL раньше можно было встретить более мягкое поведение: база могла вернуть какое-то значение из группы. Но в строгом режиме с ONLY_FULL_GROUP_BY MySQL тоже требует нормальной логики группировки.

Практический совет простой:

Пиши запросы так, как будто база всегда строгая. Это защитит от странных отчётов и неприятных сюрпризов.

Как читать запрос с GROUP BY

Возьмём запрос:

SELECT
  category,
  COUNT(*) AS products_count,
  AVG(price) AS avg_price
FROM products
WHERE is_active = true
GROUP BY category
HAVING COUNT(*) >= 5
ORDER BY avg_price DESC;

Читать его удобно не сверху вниз, а по смыслу:

  1. Берём таблицу products.
  2. Оставляем только активные товары.
  3. Группируем товары по категории.
  4. Для каждой категории считаем количество и среднюю цену.
  5. Оставляем только категории, где товаров минимум 5.
  6. Сортируем категории по средней цене от большей к меньшей.

Такой способ чтения помогает не путаться, особенно когда запрос становится длиннее.

Когда использовать GROUP BY

Используй GROUP BY, когда в задаче звучат слова:

  • «по каждому»;
  • «по категориям»;
  • «по странам»;
  • «по месяцам»;
  • «сколько всего»;
  • «сумма по»;
  • «среднее по»;
  • «минимум и максимум по»;
  • «топ по количеству».

Например:

«Сколько заказов у каждого клиента?» — почти точно нужен GROUP BY customer_id.

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id;

«Какая выручка по каждому месяцу?» — нужен GROUP BY по месяцу.

SELECT
  DATE_TRUNC('month', created_at) AS month,
  SUM(amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;

«Какая средняя цена товара в каждой категории?» — нужен GROUP BY category.

SELECT
  category,
  AVG(price) AS avg_price
FROM products
GROUP BY category;

Главное из статьи

GROUP BY — это способ собрать строки в группы и посчитать итог по каждой группе.

Было много строк:

customer_id amount
101 500
101 700
102 300

Стало по одной строке на группу:

customer_id total_amount
101 1200
102 300

Главные правила:

  • GROUP BY column группирует строки по одинаковым значениям в колонке.
  • GROUP BY a, b группирует по комбинации значений.
  • В SELECT можно указывать только колонки из GROUP BY и агрегатные функции.
  • COUNT(*) считает все строки.
  • COUNT(column) считает только строки, где column не NULL.
  • WHERE фильтрует строки до группировки.
  • HAVING фильтрует группы после группировки.
  • DISTINCT нужен для списка уникальных значений.
  • GROUP BY нужен для расчётов по группам.
  • Все NULL в группируемой колонке попадают в одну группу.
  • Для группировки по месяцам часто удобнее использовать DATE_TRUNC.

Если сказать совсем просто: GROUP BY — это инструмент для отчётов. Он берёт большую таблицу с отдельными событиями и превращает её в понятную сводку: по клиентам, странам, категориям, месяцам и любым другим группам.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр