new-lvl.pro · Статьи · GROUP BY
Статья SQL // 11 мин чтения

GROUP BY и HAVING

Чем HAVING отличается от WHERE, почему нельзя фильтровать по COUNT в WHERE и откуда берётся ошибка «column must appear in the GROUP BY clause». Все ответы дают одну и ту же вещь — порядок выполнения запроса. Внутри пошаговый интерактив: видно, что происходит со строками на каждой стадии.

WHERE или HAVING — короткий ответ

WHEREHAVING
Что фильтруетОтдельные строкиГотовые группы
Когда работаетДо группировкиПосле группировки
Агрегаты внутриНельзя — групп ещё нетМожно — ради этого и нужен
Типичное условиеstatus = 'paid'COUNT(*) > 10
Нужен ли GROUP BYНетОбычно да
// Правило одной строки
Условие про значение в строке — в WHERE. Условие про агрегат по группе — в HAVING. Если условие можно поставить в WHERE, ставь туда: база отбросит строки раньше и будет группировать меньше данных.

Запрос выполняется не в том порядке, в котором написан

Это единственная идея, которую нужно понять. Из неё следуют ответы на все остальные вопросы темы — почему агрегат не работает в WHERE, откуда берётся ошибка про GROUP BY и почему алиас из SELECT не виден в WHERE, но виден в ORDER BY.

Мы пишем запрос начиная с SELECT, а база выполняет его почти с конца:

1
FROM / JOIN
Взять таблицы и соединить их
2
WHERE
Отбросить лишние строки. Групп ещё нет — агрегаты недоступны
3
GROUP BY
Разложить оставшиеся строки по группам
4
HAVING
Отбросить лишние группы. Агрегаты уже посчитаны
5
SELECT
Выбрать колонки и присвоить алиасы — только здесь они появляются
6
ORDER BY
Отсортировать. Алиасы из SELECT уже видны
7
LIMIT
Оставить первые N строк

Обрати внимание на пятый шаг. SELECT пишется первым, а выполняется предпоследним — поэтому алиас, заданный в нём, не существует на этапе WHERE, но прекрасно работает в ORDER BY.

Пошаговое выполнение: что происходит со строками

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

// Выполнение запроса по шагам
Шаг 1 из 6
// Два места, где всё становится понятно
Шаг 2: отменённый заказ Питера на 900 ₽ отбрасывается ещё до группировки — поэтому в итоге у Питера 1500, а не 2400. Это и есть работа WHERE.

Шаг 5: Казань набрала ровно 1000 и не проходит условие > 1000. Отбрасывается целая группа, а не строка — так работает HAVING.

GROUP BY: строки схлопываются в группы

GROUP BY берёт строки и раскладывает их по корзинам: в одну корзину попадают строки с одинаковым значением в указанных колонках. Дальше каждая корзина превращается в одну строку результата.

Отсюда главное следствие: после группировки исходных строк больше не существует. Есть только группы и то, что можно про них посчитать.

// Что можно писать в SELECT после GROUP BY
Ровно две вещи
колонки из GROUP BY · агрегатные функции
Всё остальное вызовет ошибку. База не знает, какое из нескольких значений колонки подставить в единственную строку группы, — и честно об этом сообщает.
Самая частая ошибка новичка
SQL
-- ❌ ERROR: column "o.id" must appear in the GROUP BY clause
SELECT city, id, SUM(sum)
FROM orders
GROUP BY city;
-- в группе «Москва» три разных id — какой из них показать?

-- ✅ Вариант 1: добавить колонку в группировку
SELECT city, id, SUM(sum) FROM orders GROUP BY city, id;

-- ✅ Вариант 2: обернуть в агрегат — «покажи любой один»
SELECT city, MAX(id), SUM(sum) FROM orders GROUP BY city;
// Осторожно с вариантом 1
Добавить колонку в GROUP BY кажется самым быстрым способом убрать ошибку — и часто это неверное решение. Добавив id, ты получишь группы по одной строке, и агрегация потеряет смысл. Сначала реши, по чему группируешь, и только потом чини ошибку.

Группировка по нескольким колонкам

В GROUP BY можно перечислить несколько колонок — тогда группой считается уникальное сочетание их значений. GROUP BY city, status даст группы «Москва + paid», «Москва + cancelled» и так далее. Число групп при этом растёт быстро, поэтому разрезов лучше брать столько, сколько реально нужно в отчёте.

Агрегатные функции и NULL

Здесь живёт большинство тихих ошибок в отчётах: агрегаты по-разному относятся к пустым значениям, и это нигде не подсвечивается.

ФункцияЧто делает с NULLПример на 3 строках: 10, 20, NULL
COUNT(*)Считает все строки3
COUNT(v)Пропускает NULL2
SUM(v)Пропускает NULL30
AVG(v)Пропускает NULL — делит на 2, не на 315, а не 10
MIN/MAX(v)Пропускают NULL10 / 20

Разница между AVG = 15 и «средним по всем трём строкам» = 10 — ровно тот случай, когда отчёт выглядит правдоподобно и при этом неверен. Если пустое значение по смыслу означает ноль, его нужно заменить явно: AVG(COALESCE(v, 0)).

// Ловушка после LEFT JOIN
COUNT(*) для пользователя без заказов вернёт 1, а не 0: строка-то существует, просто заполнена NULL-ами. Нужен COUNT(o.id) — он считает только непустые значения и честно даст 0. Подробнее про это в статье о типах JOIN.

COUNT(DISTINCT …) — считаем уникальные

Отдельная форма для подсчёта уникальных значений. Именно так считают DAU: не число событий, а число разных пользователей.

События против пользователей
SQL
SELECT
    event_date,
    COUNT(*)                    AS events,   -- сколько действий
    COUNT(DISTINCT user_id)     AS dau       -- сколько людей
FROM events
GROUP BY event_date
ORDER BY event_date;

NULL как отдельная группа

В обычных сравнениях NULL != NULL, но GROUP BY — исключение: все строки с пустым значением попадают в одну общую группу. В результате появится строка, где вместо названия группы стоит NULL. Если она не нужна, отсекай её до группировки через WHERE city IS NOT NULL.

HAVING: то же самое, но по группам

HAVING — это WHERE, приехавший на четыре шага позже. Он получает уже посчитанные группы и решает, какие из них оставить.

Оба фильтра в одном запросе
SQL
SELECT city, COUNT(*) AS orders, SUM(sum) AS revenue
FROM orders
WHERE status = 'paid'          -- фильтр строк: до группировки
GROUP BY city
HAVING SUM(sum) > 1000        -- фильтр групп: после
ORDER BY revenue DESC;         -- алиас виден: ORDER BY идёт после SELECT

Попытка написать WHERE SUM(sum) > 1000 закончится ошибкой, и теперь понятно почему: на шаге WHERE групп ещё нет, а значит, и суммировать нечего.

// Про производительность
Одно и то же условие иногда можно поставить в оба места. Правило: сначала WHERE. Он отбрасывает строки до группировки — база сортирует и группирует меньший объём. HAVING оставляй только для того, что без агрегата не выражается.

5 ошибок с GROUP BY

Агрегат в WHERE. WHERE COUNT(*) > 5 не работает никогда — на этом шаге групп ещё нет. Только HAVING.
Лишняя колонка в GROUP BY. Добавили id, чтобы убрать ошибку, — получили группы по одной строке и бессмысленную агрегацию.
COUNT(*) вместо COUNT(колонка). После LEFT JOIN даёт 1 там, где должен быть 0.
AVG по данным с NULL. Делит на число непустых, а не на все строки. Если NULL означает ноль — нужен COALESCE.
Фильтр строк в HAVING. Работать может, но группировать придётся лишние данные. Такое условие — в WHERE.
Забытый NULL-разрез. Строка с пустым значением группы легко теряется при чтении отчёта, хотя может оказаться самой крупной.

Проверь себя

Задачи на том же наборе заказов, что и в интерактиве выше.

1
Сколько строк вернёт запрос, если убрать из него HAVING SUM(sum) > 1000?
Показать ответ
4 строки — по одной на каждый город с оплаченными заказами: Москва 2000, Питер 1500, Казань 1000, Сочи 300. HAVING отбрасывал две группы из четырёх.
2
Почему Казань не попала в результат, если её сумма ровно 1000?
Показать ответ
Условие строгое: > 1000, а не >= 1000. Ровно тысяча его не проходит. Классическая причина расхождения на одну строку между отчётом и ожиданием — стоит проверять первым делом.
3
В данных у Питера два заказа на 1500 и 900. Почему в результате у него orders = 1 и revenue = 1500?
Показать ответ
Заказ на 900 имеет статус cancelled и отбрасывается фильтром WHERE status = 'paid'ещё до группировки. В группу «Питер» он не попадает вовсе, поэтому и в COUNT, и в SUM его нет. Если бы этот фильтр стоял в HAVING, он бы не сработал так: к тому моменту заказ уже был бы просуммирован.
4
В таблице 6 строк, в колонке promo три значения 'SALE' и три NULL. Что вернут COUNT(*), COUNT(promo) и COUNT(DISTINCT promo)?
Показать ответ
COUNT(*) = 6 — все строки.
COUNT(promo) = 3 — только непустые.
COUNT(DISTINCT promo) = 1 — различное непустое значение всего одно, 'SALE'.
NULL не считается ни во второй, ни в третьей форме.

Частые вопросы про GROUP BY и HAVING

Чем HAVING отличается от WHERE?
WHERE фильтрует отдельные строки и работает до группировки — групп ещё нет, поэтому агрегат в нём использовать нельзя. HAVING фильтрует готовые группы и работает после GROUP BY, поэтому COUNT, SUM и остальные агрегаты в нём доступны. Правило: условие на значение строки — в WHERE, условие на агрегат группы — в HAVING.
Можно ли использовать WHERE и HAVING в одном запросе?
Да, это обычная практика. WHERE отсечёт лишние строки до группировки, HAVING — лишние группы после. Если условие можно поставить в WHERE, ставьте туда: база отбросит строки раньше и обработает меньше данных.
Почему возникает ошибка «column must appear in the GROUP BY clause»?
В SELECT указана колонка, которой нет ни в GROUP BY, ни внутри агрегата. После группировки каждая группа — одна строка, и база не знает, какое из нескольких значений в неё подставить. Решения два: добавить колонку в GROUP BY или обернуть в агрегат (MIN, MAX, STRING_AGG). Прежде чем выбирать, решите, по чему вы вообще группируете.
Чем COUNT(*) отличается от COUNT(столбец)?
COUNT(*) считает все строки группы, включая те, где в колонках NULL. COUNT(столбец) считает только строки с непустым значением. COUNT(DISTINCT столбец) — число различных непустых значений. Отсюда типичная ошибка: после LEFT JOIN нужен COUNT колонки правой таблицы, иначе у записей без пары выйдет 1 вместо 0.
Как GROUP BY обрабатывает NULL?
Все строки с NULL в колонке группировки попадают в одну общую группу — хотя в обычных сравнениях NULL не равен NULL. В результате появится строка с NULL вместо названия группы. Не нужна — отсекайте через WHERE колонка IS NOT NULL до группировки.
Можно ли использовать алиас из SELECT в WHERE?
Нет: SELECT выполняется после WHERE, и на момент фильтрации алиаса ещё не существует. В ORDER BY — можно, он идёт последним. PostgreSQL дополнительно разрешает алиас в GROUP BY, MySQL — ещё и в HAVING, но это расширения диалектов, и переносимый код на них лучше не закладывать.

Связанные материалы

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

Вся тема держится на одном факте: запрос выполняется не в том порядке, в котором написан. FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.

WHERE работает до группировки и фильтрует строки — агрегата в нём быть не может. HAVING работает после и фильтрует группы. SELECT выполняется предпоследним, поэтому его алиасы не видны в WHERE, но видны в ORDER BY. А ошибка про GROUP BY clause означает ровно одно: в SELECT попала колонка, которую группа не может представить одним значением.

Практика: возьми любой свой отчёт с GROUP BY и проверь агрегаты на NULL. Если где-то есть AVG по колонке с пустыми значениями — цифра почти наверняка завышена.

АТ
Андрей Тарасенко
// Продуктовый аналитик · Авито · Ментор

Порядок выполнения запроса — та вещь, которую я рисую менти на первой же SQL-встрече. После неё вопросы «почему нельзя COUNT в WHERE» и «откуда эта ошибка про GROUP BY» отпадают сами: они все про одно и то же, просто с разных сторон.

Написать в Telegram
// ЗАКРЕПИ НА ПРАКТИКЕ

Напиши свой GROUP BY на живой базе

SQL-тренажёр на датасете маркетплейса: запрос выполняется по-настоящему и проверяется автоматически. Группировки, агрегаты и HAVING — с первых же задач. 20 задач бесплатно.

▶ Открыть SQL-тренажёр ★ Разобраться с JOIN
Все материалы: База знаний · Telegram