Коротко: HAVING фильтрует строки ПОСЛЕ группировки и агрегации, а WHERE — ДО неё. Поэтому в WHERE нельзя писать COUNT(), SUM() и другие агрегаты, а в HAVING — можно и нужно. Если запомнить порядок выполнения запроса (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY), 90% ошибок с «group by having» исчезают сами.
Связка GROUP BY и HAVING — один из самых частых блоков на собеседовании аналитика данных. Спрашивают не просто синтаксис, а понимание: почему агрегат в WHERE падает с ошибкой, а в HAVING работает. Разберём по шагам с примерами, которые реально встречаются в рабочих задачах.
Что делает GROUP BY?
GROUP BY схлопывает строки в группы по значениям одной или нескольких колонок. Каждая группа превращается в одну строку результата, а агрегатные функции считают значение внутри группы.
Например, у нас таблица заказов orders(user_id, amount, status). Хотим узнать сумму по каждому клиенту:
SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id
Здесь все строки с одинаковым user_id объединяются в группу, а SUM(amount) считает сумму внутри неё. На выходе — по одной строке на клиента.
Главное правило: в SELECT вместе с GROUP BY могут быть только сгруппированные колонки и агрегаты. Нельзя написать SELECT user_id, amount FROM orders GROUP BY user_id — колонка amount не входит в GROUP BY и не обёрнута в агрегат. PostgreSQL выдаст ошибку «column must appear in the GROUP BY clause or be used in an aggregate function». MySQL в старых режимах вернёт случайное значение — это ещё хуже, потому что ошибки нет, а данные мусорные.
Чем HAVING отличается от WHERE?
Это вопрос-ловушка номер один на собесах. Оба ключевых слова фильтруют строки, но на разных этапах.
| Критерий | WHERE | HAVING |
|---|---|---|
| Когда работает | до группировки | после группировки |
| С агрегатами | нельзя (COUNT(), SUM() запрещены) | можно и нужно |
| По каким полям | по исходным колонкам строк | по результатам агрегации и группировочным колонкам |
| Производительность | отсекает строки рано, дешевле | работает по уже агрегированным группам |
Простое правило: если условие про отдельную строку — это WHERE. Если условие про группу целиком (её сумму, количество, среднее) — это HAVING.
Пример. Найти клиентов, у которых больше 5 оплаченных заказов:
SELECT user_id, COUNT(*) AS cnt FROM orders WHERE status = 'paid' GROUP BY user_id HAVING COUNT(*) > 5
Разберём фильтры:
WHERE status = 'paid'— отсекает неоплаченные строки ДО группировки (условие про строку).HAVING COUNT(*) > 5— оставляет только группы с более чем 5 заказами (условие про группу).
В каком порядке выполняется SQL-запрос?
Ключ к пониманию HAVING vs WHERE — логический порядок выполнения. SQL пишется не в том порядке, в каком исполняется. Вот реальная последовательность:
- FROM / JOIN — собираются и соединяются таблицы.
- WHERE — фильтруются отдельные строки.
- GROUP BY — строки группируются.
- HAVING — фильтруются группы.
- SELECT — вычисляются выражения и алиасы колонок.
- DISTINCT — убираются дубликаты.
- ORDER BY — сортировка.
- LIMIT / OFFSET — обрезка результата.
Из этого порядка следуют все «странности»:
- WHERE не видит агрегаты, потому что выполняется ДО GROUP BY — агрегатов ещё не существует.
- HAVING видит агрегаты, потому что выполняется ПОСЛЕ группировки.
- Алиас из SELECT (например
SUM(amount) AS total) нельзя использовать в WHERE и обычно нельзя в HAVING — SELECT выполняется позже. В PostgreSQL приходится повторятьHAVING SUM(amount) > 1000, а неHAVING total > 1000. А вот в ORDER BY алиас доступен, потому что сортировка идёт после SELECT.
Этот порядок стоит выучить наизусть — на собеседовании его просят озвучить дословно. Подробный разбор агрегатов с интерактивными задачами есть на странице SQL-агрегация и группировка.
Какие типичные ошибки с GROUP BY и HAVING?
Соберём грабли, на которые наступают чаще всего.
1. Агрегат в WHERE. Классика:
- Ошибка:
SELECT user_id FROM orders WHERE COUNT(*) > 5 GROUP BY user_id - Правильно: перенести
COUNT(*) > 5в HAVING.
2. Колонка не в GROUP BY и не в агрегате. Если хотите вывести имя клиента вместе с суммой, добавьте имя в GROUP BY либо оберните в агрегат:
SELECT user_id, MAX(name), SUM(amount) FROM orders GROUP BY user_id— либоGROUP BY user_id, name.
3. Фильтр строк затолкали в HAVING. Технически HAVING умеет фильтровать по обычным колонкам, но это медленнее и нелогично:
- Плохо:
GROUP BY user_id HAVING status = 'paid' - Хорошо:
WHERE status = 'paid' GROUP BY user_id
WHERE отсекает строки рано, до группировки — групп получается меньше, запрос быстрее.
4. Забыли про NULL. COUNT(*) считает все строки группы, а COUNT(amount) — только строки, где amount не NULL. Это разные числа. Если в данных есть NULL, AVG(amount) их игнорирует — среднее считается по не-NULL значениям, а не по всем строкам.
5. HAVING без GROUP BY. Технически допустимо: HAVING тогда работает по всей таблице как по одной группе. Но это редкий случай — обычно это признак опечатки.
Как комбинировать WHERE и HAVING в одном запросе?
В реальных задачах оба фильтра работают вместе. Пример из продуктовой аналитики — найти категории товаров, где за 2026 год средний чек превысил 3000 рублей при минимум 100 заказах:
SELECT category, AVG(amount) AS avg_check, COUNT(*) AS cnt FROM orders WHERE order_date >= '2026-01-01' GROUP BY category HAVING AVG(amount) > 3000 AND COUNT(*) >= 100 ORDER BY avg_check DESC
Как это читается по этапам выполнения:
- WHERE оставляет только заказы 2026 года.
- GROUP BY группирует по категории.
- HAVING оставляет категории с высоким средним чеком и достаточным объёмом.
- ORDER BY сортирует по убыванию среднего чека.
Заметьте: фильтр по дате — в WHERE (про строку), а фильтры по среднему и количеству — в HAVING (про группу). Это и есть правильное разделение ответственности.
Когда вместо HAVING стоит взять оконную функцию или CTE?
HAVING фильтрует группы, но не даёт сравнить строку с агрегатом группы, не схлопывая её. Если нужно оставить детализацию строк, но при этом фильтровать по агрегату — берут оконные функции или CTE.
Например, «вывести все заказы клиентов, у которых суммарно больше 5 заказов»: GROUP BY + HAVING схлопнет данные до одной строки на клиента, а нам нужны все строки. Решение — посчитать COUNT(*) OVER (PARTITION BY user_id) в подзапросе и отфильтровать снаружи во WHERE. Этот переход «HAVING → оконка» — отличный follow-up вопрос на собесе, который любят задавать middle-аналитикам.
Если запросы становятся многоэтажными, выносите промежуточную агрегацию в CTE (WITH ... AS (...)) — читаемость растёт, а оптимизатор обычно справляется не хуже подзапроса.
Где потренироваться?
Теория без практики выветривается за неделю. Закрепите GROUP BY и HAVING на живых данных:
- SQL-тренажёр — десятки задач на агрегацию с настоящим PostgreSQL прямо в браузере, с автопроверкой и разбором.
- Раздел SQL-агрегация — тематическая подборка задач именно по GROUP BY, HAVING и агрегатным функциям.
- Курс «SQL с нуля» — пошаговая программа от SELECT до оконных функций, где часть 4 целиком посвящена агрегатам и группировке.
Пройдите 20-30 задач на агрегацию — и на собеседовании вопрос про разницу HAVING и WHERE вы будете отвечать на автомате, вместе с порядком выполнения запроса и типичными ловушками.