Шпаргалка
WHERE или HAVING — короткий ответ
// Правило одной строки
Условие про
значение в строке — в 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 · агрегатные функции
Всё остальное вызовет ошибку. База не знает, какое из нескольких значений колонки подставить в единственную строку группы, — и честно об этом сообщает.
-- ❌ 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
Здесь живёт большинство тихих ошибок в отчётах: агрегаты по-разному относятся к пустым значениям, и это нигде не подсвечивается.
Разница между AVG = 15 и «средним по всем трём строкам» = 10 — ровно тот случай, когда отчёт выглядит правдоподобно и при этом неверен. Если пустое значение по смыслу означает ноль, его нужно заменить явно: AVG(COALESCE(v, 0)).
// Ловушка после LEFT JOIN
COUNT(*) для пользователя без заказов вернёт
1, а не 0: строка-то существует, просто заполнена NULL-ами. Нужен
COUNT(o.id) — он считает только непустые значения и честно даст 0. Подробнее про это в статье о
типах JOIN.
COUNT(DISTINCT …) — считаем уникальные
Отдельная форма для подсчёта уникальных значений. Именно так считают DAU: не число событий, а число разных пользователей.
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, приехавший на четыре шага позже. Он получает уже посчитанные группы и решает, какие из них оставить.
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 оставляй только для того, что без агрегата не выражается.
Агрегат в 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 не считается ни во второй, ни в третьей форме.
FAQ
Частые вопросы про 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