Блог
SQL

GROUP BY vs HAVING в SQL: разница и частые ошибки

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

Как посчитать количество заказов у каждого пользователя через GROUP BY?

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

GROUP BY user_id собирает все строки orders с одинаковым user_id в одну группу, и COUNT(*) считает количество строк в каждой такой группе отдельно. Результат — одна строка на каждого уникального пользователя с числом его заказов, а не общее количество заказов по всей таблице.

Как отфильтровать группы по результату агрегатной функции через HAVING?

SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

HAVING COUNT(*) > 5 оставляет только те группы (пользователей), у которых заказов больше пяти. Этот фильтр невозможно выразить через WHERE, потому что COUNT(*) — результат агрегации по всей группе, а WHERE применяется к отдельным строкам ДО того, как они сгруппированы и агрегатная функция вообще посчитана. Порядок выполнения SQL-запроса логически такой: сначала FROM/WHERE (отбор и фильтрация исходных строк), потом GROUP BY (группировка), потом агрегатные функции и HAVING (фильтрация групп), и только в конце — SELECT и ORDER BY.

Как использовать WHERE и HAVING в одном запросе?

SELECT user_id, COUNT(*) AS orders_count
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY user_id
HAVING COUNT(*) > 5;

WHERE created_at >= '2026-01-01' отбирает только заказы за 2026 год ДО группировки — заказы за прошлые годы вообще не попадут в группы и не будут учтены в COUNT(*). HAVING COUNT(*) > 5 уже после этого отбора и группировки оставляет только пользователей, у которых больше пяти заказов именно за 2026 год. WHERE и HAVING можно использовать в одном запросе одновременно — они решают разные задачи и не заменяют друг друга.

Что произойдёт, если в SELECT есть столбец без GROUP BY и без агрегатной функции?

-- ошибка в PostgreSQL, "тихо работает не так, как ожидалось" в MySQL:
SELECT user_id, order_date, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id;

Если в SELECT есть столбец, который не входит ни в GROUP BY, ни в агрегатную функцию (здесь order_date), СУБД не может определить, какое именно значение этого столбца показать для группы — в группе может быть много разных order_date. PostgreSQL и большинство современных СУБД в строгом режиме такой запрос отклонят с ошибкой (column "order_date" must appear in the GROUP BY clause or be used in an aggregate function). Начиная с версии 5.7.5 (2015 год) режим ONLY_FULL_GROUP_BY включён в MySQL по умолчанию, поэтому актуальные версии MySQL (включая 8.0/9.x) тоже отклоняют такой запрос с похожей ошибкой — «тихий» пропуск с произвольным значением order_date из одной из строк группы возможен только если этот режим сознательно отключить вручную. Тем не менее это исторически известный источник трудноуловимых багов на старых или нестандартно настроенных инсталляциях MySQL — результат в таком режиме не детерминирован и может отличаться между запусками одного и того же запроса.

Когда использовать WHERE, а когда HAVING?

Отобрать строки по обычному столбцу до группировки. WHERE, выполняется до GROUP BY и до расчёта агрегатных функций. Пример: WHERE created_at >= '2026-01-01'.
Уменьшить объём данных перед группировкой. WHERE, до GROUP BY. Пример: отбор заказов только за нужный период.
Отфильтровать группы по результату агрегатной функции. HAVING, после GROUP BY и вычисления агрегатов. Пример: HAVING COUNT(*) > 5.
Проверить условие, которое физически не посчитать до агрегации. HAVING, после GROUP BY. Пример: HAVING COUNT(*) > 5 по группам за 2026 год.

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

Можно ли использовать HAVING без GROUP BY? Технически да в некоторых СУБД (тогда вся таблица считается одной группой), но практической пользы почти нет — если группировки нет, тот же результат обычно получают через WHERE с подзапросом на агрегат, или просто WHERE на обычные столбцы. HAVING без GROUP BY — редкий случай на практике.
Почему WHERE COUNT(*) > 5 не работает, а HAVING работает? Потому что на момент выполнения WHERE агрегатные функции ещё не посчитаны — WHERE фильтрует исходные строки таблицы до группировки, а COUNT(*) появляется только как результат группировки. HAVING выполняется после группировки и агрегации, поэтому может ссылаться на их результат.
Можно ли группировать по нескольким столбцам одновременно? Да, GROUP BY column1, column2 создаёт отдельную группу для каждой уникальной комбинации значений обоих столбцов, а не отдельно по каждому. Например, GROUP BY user_id, DATE(created_at) посчитает заказы каждого пользователя отдельно по каждому дню, а не суммарно за всё время.
В HAVING можно использовать псевдонимы (алиасы) из SELECT? Зависит от СУБД, и здесь PostgreSQL и MySQL расходятся. MySQL как расширение стандарта позволяет ссылаться на алиас агрегатной функции из SELECT прямо в HAVING (SELECT COUNT(*) AS cnt ... HAVING cnt > 5). PostgreSQL так не умеет — алиас из SELECT на момент выполнения HAVING ещё не виден, и такой запрос упадёт с ошибкой column "cnt" does not exist. В PostgreSQL (и в старых версиях Oracle) агрегатную функцию нужно повторить в HAVING целиком (HAVING COUNT(*) > 5), а не через алиас — это самая переносимая форма записи между СУБД.

Закрепить GROUP BY и HAVING на практике

Расхождение между PostgreSQL и MySQL в том, виден ли алиас из SELECT в HAVING (разобранное выше column "cnt" does not exist), — ровно тот тип различия, который легко пропустить в теории и тут же словить на практике при смене СУБД. Проверить обе СУБД на одних и тех же задачах на группировку и агрегацию можно на тренажёре SQL Arena от Quality Academy — диалект переключается одной кнопкой между PostgreSQL, MySQL и ClickHouse. Больше 800 задач, значительная часть бесплатно.

-------

Полезные ссылки школы

Сайт Quality Academy:
https://quality-academy.ru/main
Telegram-канал:
https://t.me/quality_academy
YouTube-канал:
https://www.youtube.com/@quality_academy
ВКонтакте:
https://vk.com/quality_academy
Канал отзывов учеников (58+ отзывов):
https://t.me/quality_academy_reviews
Задать вопрос менеджеру:
https://t.me/quality_academy_bot
Тест «Подойдёт ли вам тестирование»:
https://quiz.quality-academy.ru
5000+ вопросов с собесов тестировщика — бот-тренажёр:
https://t.me/quality_academy_interview_bot
3 практические задачи и дорожная карта:
https://t.me/quality_academy_tasks_bot
Бесплатные тренажёры — SQL Arena, Playwright Arena, Python Arena:
SQL Arena · Playwright Arena · Python Arena