GROUP BY (группировка в SQL) — это оператор, который объединяет строки с одинаковым значением в указанных столбцах в одну группу, чтобы посчитать по каждой группе агрегат: количество, сумму, среднее, минимум или максимум. Без GROUP BY агрегатная функция считает по всей таблице сразу, с ним — отдельно по каждой группе.
Содержание
Разберу оператор на одной игрушечной таблице продаж: покажу данные, соберу группировку с COUNT, SUM, AVG, MIN и MAX, разведу HAVING и WHERE и покажу три частые ошибки с реальным выводом. Примеры на стандартном SQL, поведение конкретной СУБД помечаю отдельно.
Синтаксис GROUP BY
Полная форма запроса с группировкой выглядит так:
SELECT столбцы_группировки, агрегат(...)
FROM таблица
WHERE фильтр_строк -- до группировки
GROUP BY столбцы_группировки
HAVING фильтр_групп -- после группировки, по агрегатам
ORDER BY столбцы; -- сортировка результата
Важно понимать логический порядок выполнения, он объясняет почти все ошибки новичков: сначала FROM, потом WHERE (отсев строк), затем GROUP BY (сборка групп), потом HAVING (отсев групп), затем SELECT и в конце ORDER BY.
Из этого порядка следует главное правило: в SELECT можно ставить только столбцы из GROUP BY и агрегатные функции. Любой другой столбец после группировки не имеет одного значения на группу, и СУБД не знает, какое из значений вернуть.
Таблица для примеров
Заведу таблицу sales — простое представление продаж по трем точкам. Столбцы: место продажи, продукт, цена в рублях и время продажи.
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
location TEXT,
product TEXT,
price INTEGER, -- цена в рублях
sold_at TIMESTAMP
);
INSERT INTO sales (id, location, product, price, sold_at) VALUES
(1, 'HQ', 'coffee', 200, '2026-09-14 09:15'),
(2, 'HQ', 'bagel', 120, '2026-09-14 09:40'),
(3, 'HQ', 'coffee', 200, '2026-09-13 10:05'),
(4, 'HQ', 'bagel', 120, '2026-09-12 11:20'),
(5, 'Downtown', 'coffee', 220, '2026-09-14 08:30'),
(6, 'Downtown', 'bagel', 130, '2026-09-13 12:10'),
(7, '1st Street', 'coffee', 210, '2026-09-14 14:00'),
(8, '1st Street', 'coffee', 210, '2026-09-12 15:45');
Восемь строк: четыре продажи в HQ, по две в Downtown и на 1st Street. Вот содержимое таблицы:
id | location | product | price | sold_at
----+------------+---------+-------+------------------
1 | HQ | coffee | 200 | 2026-09-14 09:15
2 | HQ | bagel | 120 | 2026-09-14 09:40
3 | HQ | coffee | 200 | 2026-09-13 10:05
4 | HQ | bagel | 120 | 2026-09-12 11:20
5 | Downtown | coffee | 220 | 2026-09-14 08:30
6 | Downtown | bagel | 130 | 2026-09-13 12:10
7 | 1st Street | coffee | 210 | 2026-09-14 14:00
8 | 1st Street | coffee | 210 | 2026-09-12 15:45
Группировка с агрегатами
Сначала посчитаю число продаж в каждой точке. COUNT(*) считает строки внутри каждой группы:
SELECT location, COUNT(*) AS number_of_sales
FROM sales
GROUP BY location;
location | number_of_sales
------------+-----------------
HQ | 4
Downtown | 2
1st Street | 2
GROUP BY взял восемь строк, нашел три уникальных значения location и по каждому посчитал строки. Порядок строк в выводе без ORDER BY не гарантирован — к этому вернусь в разделе про ошибки.
SUM складывает значения столбца в группе. Считаю выручку по точкам и сразу сортирую по убыванию:
SELECT location, SUM(price) AS revenue
FROM sales
GROUP BY location
ORDER BY revenue DESC;
location | revenue
------------+---------
HQ | 640
1st Street | 420
Downtown | 350
AVG, MIN и MAX работают так же и сочетаются в одном запросе. AVG возвращает дробное число, поэтому оборачиваю его в ROUND — без этого форматирование дробной части зависит от СУБД:
SELECT location,
ROUND(AVG(price), 2) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price
FROM sales
GROUP BY location
ORDER BY location;
location | avg_price | min_price | max_price
------------+-----------+-----------+-----------
1st Street | 210.00 | 210 | 210
Downtown | 175.00 | 130 | 220
HQ | 160.00 | 120 | 200
Группировать можно сразу по нескольким столбцам — тогда группа задается комбинацией их значений. Разложу продажи по паре «точка + продукт»:
SELECT location, product,
COUNT(*) AS sales_count,
SUM(price) AS revenue
FROM sales
GROUP BY location, product
ORDER BY location, product;
location | product | sales_count | revenue
------------+---------+-------------+---------
1st Street | coffee | 2 | 420
Downtown | bagel | 1 | 130
Downtown | coffee | 1 | 220
HQ | bagel | 2 | 240
HQ | coffee | 2 | 400
Вместо трех групп по точкам получилось пять групп по уникальным парам значений. Если оставить в SELECT только один столбец группировки без агрегатов, GROUP BY вернет уникальные значения — в этом частном случае он ведет себя как DISTINCT.
HAVING и WHERE — в чем разница
Оба слова фильтруют, но на разных этапах. WHERE отсекает исходные строки до группировки и не умеет работать с агрегатами. HAVING отсекает уже готовые группы по значению агрегата. Это прямое следствие порядка выполнения: WHERE отрабатывает раньше GROUP BY, HAVING — позже.
| Признак | WHERE | HAVING |
|---|---|---|
| Когда фильтрует | до группировки, по строкам | после группировки, по группам |
| С чем работает | значения столбцов исходных строк | результаты агрегатов и столбцы группировки |
| Агрегаты внутри условия | нельзя | можно и нужно |
| Требует GROUP BY | нет | как правило да |
| Порядок выполнения | раньше | позже |
Пример на HAVING: оставить только точки, где выручка превысила 400. Условие стоит на агрегате SUM(price), поэтому ему место в HAVING, а не в WHERE:
SELECT location, SUM(price) AS revenue
FROM sales
GROUP BY location
HAVING SUM(price) > 400
ORDER BY revenue DESC;
location | revenue
------------+---------
HQ | 640
1st Street | 420
Downtown с выручкой 350 в результат не попал. Часто нужны оба фильтра сразу: WHERE сужает строки до группировки, HAVING — группы после. Посчитаю продажи только кофе и оставлю точки, где таких продаж хотя бы две:
SELECT location, COUNT(*) AS coffee_sales
FROM sales
WHERE product = 'coffee'
GROUP BY location
HAVING COUNT(*) >= 2
ORDER BY location;
location | coffee_sales
------------+--------------
1st Street | 2
HQ | 2
WHERE product = 'coffee' оставил пять строк (HQ — 2, Downtown — 1, 1st Street — 2), затем HAVING отсеял Downtown с единственной продажей кофе.
Три частые ошибки
Ошибка 1: столбец в SELECT не в GROUP BY и не в агрегате. Пытаюсь вывести product рядом с COUNT(*), но группирую только по location:
SELECT location, product, COUNT(*)
FROM sales
GROUP BY location;
Результат — ошибка. В PostgreSQL:
ERROR: column "sales.product" must appear in the GROUP BY clause
or be used in an aggregate function
В MySQL с включенным по умолчанию режимом ONLY_FULL_GROUP_BY сообщение похоже: ERROR 1055 (42000): ... nonaggregated column 'sales.product' .... А вот SQLite и MySQL без ONLY_FULL_GROUP_BY запрос не отклонят — они молча вернут произвольное значение product из группы (правдоподобный, но неверный результат), и это опаснее явной ошибки. Причина: у группы HQ два разных продукта, и СУБД либо не знает, какой вернуть, либо берет случайный. Исправление — либо добавить столбец в группировку, либо агрегировать его:
SELECT location, product, COUNT(*)
FROM sales
GROUP BY location, product; -- product теперь часть ключа группы
Ошибка 2: условие на агрегат в WHERE. Фильтрую выручку через WHERE, а не HAVING:
SELECT location, SUM(price) AS revenue
FROM sales
WHERE SUM(price) > 400
GROUP BY location;
WHERE выполняется до группировки, когда агрегатов еще нет, поэтому запрос падает. PostgreSQL: ERROR: aggregate functions are not allowed in WHERE. MySQL: ERROR 1111 (HY000): Invalid use of group function. Исправление — перенести условие в HAVING, как в примере выше.
Ошибка 3: расчет на то, что GROUP BY сам сортирует. Читаю результат группировки, полагаясь на «привычный» порядок:
SELECT location, COUNT(*) AS n
FROM sales
GROUP BY location; -- порядок строк не гарантирован
Современные PostgreSQL и MySQL 8 не обещают никакой сортировки по GROUP BY, порядок может измениться от запроса к запросу и от версии. Исправление — задавать порядок явно:
SELECT location, COUNT(*) AS n
FROM sales
GROUP BY location
ORDER BY location;
Выводы
- GROUP BY объединяет строки с одинаковыми значениями выбранных столбцов в группы и позволяет посчитать по каждой группе агрегат.
- В
SELECTпри группировке допустимы только столбцы изGROUP BYи агрегатные функции — остальное вызовет ошибку. - Основные агрегаты:
COUNT(число строк),SUM(сумма),AVG(среднее),MINиMAX(крайние значения); их можно сочетать в одном запросе. WHEREфильтрует строки до группировки и не работает с агрегатами,HAVINGфильтрует готовые группы по значению агрегата.- Группировка по нескольким столбцам дает группы по комбинации значений; порядок вывода без
ORDER BYне гарантирован.
Где применяется / связь с практикой
Группировка — основа почти любой аналитики на SQL: отчеты по выручке, количеству заказов, среднему чеку, активности пользователей по дням. Как только в задаче звучит «по каждому» или «в разрезе», за этим стоит GROUP BY с агрегатами и, часто, HAVING для отбора значимых групп.
Освойте тему на практике
Отработать группировку вместе с джойнами, оконными функциями и оптимизацией запросов на реальных задачах можно на курсе sql в Otus. Посмотреть формат и уровень занятий, не оплачивая курс, помогут бесплатные вебинары — там разбирают конкретные запросы и отвечают на вопросы.
Смежные темы: Команда Select в SQL, SQL для начинающих: что нужно знать о языке и его применении, Изучение SQL-команд на базе MySQL.
FAQ
Можно ли использовать GROUP BY без агрегатных функций?
Да. Тогда запрос вернет уникальные комбинации значений столбцов группировки — результат совпадет с SELECT DISTINCT по тем же столбцам.
Чем GROUP BY отличается от DISTINCT?
DISTINCT просто убирает повторяющиеся строки результата. GROUP BY формирует группы, к которым можно применить агрегаты и отбор через HAVING, — без этого на одном столбце их выводы совпадают, но возможности у GROUP BY шире.
Как сгруппировать по дате, если в столбце хранится дата со временем?
Нужно группировать по вычисляемому значению: GROUP BY CAST(sold_at AS DATE) или через функцию извлечения даты. Конкретная функция зависит от СУБД (например, DATE(sold_at) в MySQL, sold_at::date в PostgreSQL).



