Зачем два разных фильтра
Допустим, тебе нужен список «клиентов, у которых больше 2 заказов». Это два разных вопроса в одном:
- «Кого считать заказом» — может быть, только
delivered? Это фильтр по строкам до группировки. - «Какие группы оставить» — те, где счётчик больше 2. Это фильтр по результату группировки.
Эти два фильтра принципиально разные, потому что они работают на разных уровнях: один — на исходных кортежах, другой — на агрегированных. Поэтому в SQL для них два разных ключевых слова: WHERE и HAVING.
Порядок логической обработки запроса
В стандарте SQL зафиксирован
Это концептуальная модель: WHERE применяется к исходным кортежам, HAVING — к результату агрегации. Оптимизатор может физически переставить шаги, но результат должен быть таким, как будто шло в этом порядке.
Главное: WHERE отрабатывает на шаге 3, до группировки. HAVING — на шаге 5, после. Из этого вытекает всё практическое поведение.
WHERE — фильтр до группировки
WHERE отсекает строки до того, как GROUP BY начнёт что-либо группировать. Внутри WHERE нельзя использовать агрегаты — потому что агрегатов на этом этапе ещё не существует.
Считаем число доставленных заказов у каждого клиента — фильтр по статусу в WHERE:
Сначала WHERE status = 'delivered' оставил только delivered-заказы. Потом GROUP BY customer_id разрезал на группы. Потом COUNT(*) посчитал, сколько delivered-заказов у каждого клиента.
Если попытаться вставить агрегат в WHERE, будет ошибка:
Ошибка: aggregate functions are not allowed in WHERE:
PostgreSQL вернёт aggregate functions are not allowed in WHERE. Это логично: на момент обработки WHERE групп ещё нет, считать COUNT(*) не от чего.
HAVING — фильтр после группировки
HAVING работает на уровне групп, после того как агрегаты уже посчитаны. Внутри HAVING агрегаты разрешены — это и есть его главное предназначение.
Клиенты, у которых больше одного заказа любого статуса:
Здесь HAVING COUNT(*) > 1 оставляет только те группы, где счётчик больше единицы. Группы с одним заказом отфильтрованы из результата.
Можно ли в HAVING писать не-агрегаты?
Технически да, но только колонки, входящие в GROUP BY. Это эквивалентно WHERE, и почти всегда стоит написать в WHERE — будет понятнее и эффективнее.
Два эквивалентных запроса. Второй — анти-паттерн, читается хуже:
Правило стиля: фильтр на сырые колонки — в WHERE. Фильтр на агрегаты — в HAVING. Это правило не оптимизация (оптимизатор всё равно push-down-ит фильтры), а правило ясности кода: читающий сразу видит, что является «отсечкой строк», а что «отсечкой групп».
Часто встречающиеся комбинации
Реальный запрос обычно использует оба:
Клиенты с минимум 2 доставленными заказами:
Сначала WHERE status = 'delivered' оставил только delivered-заказы. Потом GROUP BY c.id разрезал на клиентов. Потом HAVING COUNT(*) >= 2 оставил только тех, у кого таких заказов 2 или больше.
HAVING без GROUP BY: вся таблица как одна группа
Что произойдёт, если написать HAVING без GROUP BY? Это легально — и означает «считай всю таблицу одной группой».
HAVING без GROUP BY: вернёт строку, только если условие на всю таблицу истинно:
Если в orders 20 заказов — запрос вернёт total_orders = 20. Если бы было 5 — вернул бы пустой результат (ноль строк), потому что условие 5 > 10 ложно для единственной «группы».
Сравни с похожим запросом без HAVING:
Без HAVING: всегда возвращает строку с числом:
Этот всегда вернёт ровно одну строку — даже если таблица пустая (тогда COUNT(*) = 0). А запрос выше с HAVING COUNT(*) > 10 может вернуть ноль строк. Это разница между «есть результат, но он 0» и «результата нет».
HAVING vs подзапрос — два способа фильтрации по агрегату
Иногда хочется написать «клиенты, у которых > 2 заказов» через подзапрос. Это работает, но обычно длиннее:
Эквивалент через подзапрос — работает, но многословно:
Внутри подзапроса — обычный GROUP BY без HAVING. Снаружи — обычный WHERE по уже посчитанной колонке. Результат тот же, но кода больше. HAVING — это синтаксический сахар, чтобы избежать такой обёртки.
Алиас из SELECT в HAVING — нельзя в стандарте, но можно в Postgres
Из-за логического порядка SELECT идёт после HAVING, поэтому стандарт запрещает использовать алиасы из SELECT в HAVING. PostgreSQL позволяет — это его расширение:
В Postgres можно ссылаться на алиас из SELECT в HAVING (расширение):
Совет: если пишешь портируемый SQL, повторяй агрегат в HAVING явно — HAVING COUNT(*) > 1. Это работает везде.
Чек-лист
WHEREотрабатывает доGROUP BY— на сырых кортежах. Агрегаты внутри запрещены.HAVINGотрабатывает послеGROUP BY— на группах. Агрегаты внутри разрешены и ожидаемы.- Фильтр на сырую колонку →
WHERE. Фильтр на агрегат →HAVING. Это правило стиля. HAVINGбезGROUP BYтрактует всю таблицу как одну группу — может вернуть либо одну строку, либо ноль строк.HAVINGэквивалентен подзапросуWHEREповерх агрегации, просто короче.- В PostgreSQL можно использовать алиасы из
SELECTвHAVING(расширение). В переносимом коде лучше повторять агрегат явно.