GROUP BY — это команда «схлопни одинаковые строки в одну и посчитай по ним что-нибудь». Самый базовый способ получить статистику: «сколько заказов у каждого клиента», «сумма продаж по каждому товару», «средняя зарплата по департаменту».
Представь таблицу заказов. Без GROUP BY каждая строка — отдельный заказ, и их сотни. С GROUP BY customer_id все заказы одного клиента превращаются в одну строку, в которой можно посмотреть итоги: сколько у него заказов всего, на какую сумму, последний когда.
Зачем нужен GROUP BY
SELECT без GROUP BY отвечает на вопрос «покажи мне строки». С GROUP BY — на вопрос «покажи мне сводку по группам». Это две разные задачи.
Типичные сценарии:
- Сколько пользователей зарегистрировалось в каждой стране.
- Какая средняя цена товара в каждой категории.
- Сколько раз каждый клиент совершал покупку.
- Топ-10 самых популярных тегов на блоге.
GROUP BY плюс агрегатная функция (COUNT, SUM, AVG, MIN, MAX) — это и есть «отчёт».
Базовый синтаксис
SELECT country, COUNT(*) AS users_count
FROM users
GROUP BY country;
Что происходит:
- Postgres берёт все строки из
users.
- Группирует их по
country — все одинаковые страны вместе.
- Для каждой группы выполняет
COUNT(*) — считает строки в группе.
- Возвращает по одной строке на группу.
Пример с таблицей
Есть таблица users:
| id |
name |
country |
| 1 |
Аня |
RU |
| 2 |
Боб |
US |
| 3 |
Вера |
RU |
| 4 |
Гриша |
RU |
| 5 |
Дима |
US |
Запрос:
SELECT country, COUNT(*) AS users_count
FROM users
GROUP BY country;
Результат:
| country |
users_count |
| RU |
3 |
| US |
2 |
5 строк превратились в 2 — одна на каждую страну. У России три юзера (Аня, Вера, Гриша), у США два (Боб, Дима).
Самые частые агрегаты
SELECT
category,
COUNT(*) AS items_count,
SUM(price) AS total_price,
AVG(price) AS avg_price,
MIN(price) AS cheapest,
MAX(price) AS expensive
FROM products
GROUP BY category;
Каждая группа теперь даёт одну строку со всеми этими цифрами сразу.
GROUP BY по нескольким колонкам
Можно группировать по комбинации:
SELECT country, tier, COUNT(*) AS users_count
FROM users
GROUP BY country, tier;
Группа = пара (country, tier). Если есть Россия с tier=free и Россия с tier=gold — это две разные группы.
Результат может выглядеть так:
| country |
tier |
users_count |
| RU |
free |
47 |
| RU |
gold |
12 |
| US |
free |
23 |
| US |
gold |
8 |
Главное правило: что можно в SELECT
В SELECT после GROUP BY можно класть только две вещи:
- Колонки, по которым группируешь (
country в нашем примере).
- Агрегатные функции (
COUNT, SUM, AVG, MIN, MAX).
Положить туда что-то другое — например, name — нельзя:
SELECT country, name, COUNT(*) FROM users GROUP BY country;
Логика: в группе RU три юзера — Аня, Вера, Гриша. Какое из трёх имён показать? Postgres не угадает. Поэтому либо добавь name в GROUP BY (тогда группы станут (country, name)), либо вынеси в агрегат (MAX(name) — алфавитно последнее).
В MySQL это правило исторически было слабее — он молча возвращал «случайное» имя. Это поведение убрано в строгом режиме, но если работаешь со старыми MySQL — будь осторожен.
GROUP BY vs DISTINCT — в чём разница
Оба «схлопывают» дубликаты, но цели разные:
SELECT DISTINCT country FROM users — даёт уникальные страны (RU, US). Никаких счётчиков.
SELECT country, COUNT(*) FROM users GROUP BY country — то же самое плюс счётчик в каждой группе.
Если просто хочешь уникальные значения — DISTINCT короче. Если нужна агрегация — только GROUP BY.
ORDER BY с GROUP BY
После группировки ты можешь сортировать как обычно:
SELECT country, COUNT(*) AS users_count
FROM users
GROUP BY country
ORDER BY users_count DESC
LIMIT 5;
ORDER BY сортирует результат группировки, а не исходные строки.
Частые ошибки новичков
1. Положили в SELECT колонку, которой нет в GROUP BY. Самая типичная ошибка для новичков. Postgres падает с понятной ошибкой — но в MySQL до недавнего времени просто возвращал «любое» значение из группы.
2. Фильтрация агрегата через WHERE. WHERE фильтрует строки до группировки и не знает про COUNT/SUM. Для условий на агрегаты есть отдельный HAVING:
SELECT country, COUNT(*) FROM users
GROUP BY country
WHERE COUNT(*) > 100;
SELECT country, COUNT(*) FROM users
GROUP BY country
HAVING COUNT(*) > 100;
WHERE режет строки на входе, HAVING режет группы на выходе.
3. COUNT(*) vs COUNT(column). COUNT(*) считает все строки, включая те, где значения NULL. COUNT(email) считает только строки, где email IS NOT NULL. На пустых колонках разница огромна.
4. Сортировка по колонке, которой нет в SELECT. В ORDER BY после GROUP BY можно сортировать только по тем же колонкам, что и в SELECT, или по агрегатам (или по их алиасам).
5. NULL в GROUP BY. Все строки с NULL попадают в одну группу (Postgres трактует NULL = NULL для целей группировки). Если ожидал, что NULL пропадёт — добавь WHERE country IS NOT NULL.
6. GROUP BY по дробным числам или датам. GROUP BY price — у каждой цены своя группа, групп будет столько же, сколько уникальных значений. Обычно бессмысленно. Для дат используй DATE_TRUNC('month', created_at) — тогда группы будут по месяцам, а не посекундно.
Мини-резюме
GROUP BY column — схлопывает строки в группы, по одной на уникальное значение.
- В
SELECT можно класть только колонки из GROUP BY или агрегаты (COUNT, SUM, AVG, MIN, MAX).
- Несколько колонок —
GROUP BY a, b — группа = пара (a, b).
- Фильтр на агрегаты —
HAVING, не WHERE.
- Уникальные значения без счёта — короче через
DISTINCT.
- NULL'ы все попадают в одну группу — фильтруй заранее, если не хочешь их видеть.
GROUP BY— это команда «схлопни одинаковые строки в одну и посчитай по ним что-нибудь». Самый базовый способ получить статистику: «сколько заказов у каждого клиента», «сумма продаж по каждому товару», «средняя зарплата по департаменту».Представь таблицу заказов. Без
GROUP BYкаждая строка — отдельный заказ, и их сотни. СGROUP BY customer_idвсе заказы одного клиента превращаются в одну строку, в которой можно посмотреть итоги: сколько у него заказов всего, на какую сумму, последний когда.Зачем нужен GROUP BY
SELECTбезGROUP BYотвечает на вопрос «покажи мне строки». СGROUP BY— на вопрос «покажи мне сводку по группам». Это две разные задачи.Типичные сценарии:
GROUP BYплюс агрегатная функция (COUNT,SUM,AVG,MIN,MAX) — это и есть «отчёт».Базовый синтаксис
SELECT country, COUNT(*) AS users_count FROM users GROUP BY country;Что происходит:
users.country— все одинаковые страны вместе.COUNT(*)— считает строки в группе.Пример с таблицей
Есть таблица
users:Запрос:
SELECT country, COUNT(*) AS users_count FROM users GROUP BY country;Результат:
5 строк превратились в 2 — одна на каждую страну. У России три юзера (Аня, Вера, Гриша), у США два (Боб, Дима).
Самые частые агрегаты
SELECT category, COUNT(*) AS items_count, -- сколько строк SUM(price) AS total_price, -- сумма AVG(price) AS avg_price, -- среднее MIN(price) AS cheapest, -- минимум MAX(price) AS expensive -- максимум FROM products GROUP BY category;Каждая группа теперь даёт одну строку со всеми этими цифрами сразу.
GROUP BY по нескольким колонкам
Можно группировать по комбинации:
SELECT country, tier, COUNT(*) AS users_count FROM users GROUP BY country, tier;Группа = пара
(country, tier). Если есть Россия с tier=free и Россия с tier=gold — это две разные группы.Результат может выглядеть так:
Главное правило: что можно в SELECT
В
SELECTпослеGROUP BYможно класть только две вещи:countryв нашем примере).COUNT,SUM,AVG,MIN,MAX).Положить туда что-то другое — например,
name— нельзя:-- НЕПРАВИЛЬНО SELECT country, name, COUNT(*) FROM users GROUP BY country; -- ERROR: column "users.name" must appear in the GROUP BY clause...Логика: в группе RU три юзера — Аня, Вера, Гриша. Какое из трёх имён показать? Postgres не угадает. Поэтому либо добавь
nameвGROUP BY(тогда группы станут(country, name)), либо вынеси в агрегат (MAX(name)— алфавитно последнее).В MySQL это правило исторически было слабее — он молча возвращал «случайное» имя. Это поведение убрано в строгом режиме, но если работаешь со старыми MySQL — будь осторожен.
GROUP BY vs DISTINCT — в чём разница
Оба «схлопывают» дубликаты, но цели разные:
SELECT DISTINCT country FROM users— даёт уникальные страны (RU, US). Никаких счётчиков.SELECT country, COUNT(*) FROM users GROUP BY country— то же самое плюс счётчик в каждой группе.Если просто хочешь уникальные значения —
DISTINCTкороче. Если нужна агрегация — толькоGROUP BY.ORDER BY с GROUP BY
После группировки ты можешь сортировать как обычно:
-- Топ-5 стран по количеству юзеров SELECT country, COUNT(*) AS users_count FROM users GROUP BY country ORDER BY users_count DESC LIMIT 5;ORDER BYсортирует результат группировки, а не исходные строки.Частые ошибки новичков
1. Положили в SELECT колонку, которой нет в GROUP BY. Самая типичная ошибка для новичков. Postgres падает с понятной ошибкой — но в MySQL до недавнего времени просто возвращал «любое» значение из группы.
2. Фильтрация агрегата через WHERE.
WHEREфильтрует строки до группировки и не знает проCOUNT/SUM. Для условий на агрегаты есть отдельныйHAVING:-- НЕПРАВИЛЬНО SELECT country, COUNT(*) FROM users GROUP BY country WHERE COUNT(*) > 100; -- ПРАВИЛЬНО SELECT country, COUNT(*) FROM users GROUP BY country HAVING COUNT(*) > 100;WHEREрежет строки на входе,HAVINGрежет группы на выходе.3.
COUNT(*)vsCOUNT(column).COUNT(*)считает все строки, включая те, где значения NULL.COUNT(email)считает только строки, гдеemail IS NOT NULL. На пустых колонках разница огромна.4. Сортировка по колонке, которой нет в SELECT. В
ORDER BYпослеGROUP BYможно сортировать только по тем же колонкам, что и в SELECT, или по агрегатам (или по их алиасам).5. NULL в GROUP BY. Все строки с
NULLпопадают в одну группу (Postgres трактует NULL = NULL для целей группировки). Если ожидал, что NULL пропадёт — добавьWHERE country IS NOT NULL.6. GROUP BY по дробным числам или датам.
GROUP BY price— у каждой цены своя группа, групп будет столько же, сколько уникальных значений. Обычно бессмысленно. Для дат используйDATE_TRUNC('month', created_at)— тогда группы будут по месяцам, а не посекундно.Мини-резюме
GROUP BY column— схлопывает строки в группы, по одной на уникальное значение.SELECTможно класть только колонки изGROUP BYили агрегаты (COUNT,SUM,AVG,MIN,MAX).GROUP BY a, b— группа = пара(a, b).HAVING, неWHERE.DISTINCT.