Перейти к содержанию
Learning Platform
Урок 07.03 · 16 мин
Начальный
WHEREHAVINGGROUP BYQuery phasesLogical evaluation order

Зачем два разных фильтра

Допустим, тебе нужен список «клиентов, у которых больше 2 заказов». Это два разных вопроса в одном:

  1. «Кого считать заказом» — может быть, только delivered? Это фильтр по строкам до группировки.
  2. «Какие группы оставить» — те, где счётчик больше 2. Это фильтр по результату группировки.

Эти два фильтра принципиально разные, потому что они работают на разных уровнях: один — на исходных кортежах, другой — на агрегированных. Поэтому в SQL для них два разных ключевых слова: WHERE и HAVING.

Порядок логической обработки запроса

В стандарте SQL зафиксирован

логический порядок выполнения
предложений запроса:

Логический порядок выполнения SELECT

Это концептуальная модель: WHERE применяется к исходным кортежам, HAVING — к результату агрегации. Оптимизатор может физически переставить шаги, но результат должен быть таким, как будто шло в этом порядке.

1. FROMисточник
2. JOINсклейка
3. WHEREфильтр кортежей
4. GROUP BYразрез на группы
5. HAVINGфильтр групп
6. SELECTпроекция
7. ORDER BYсортировка

Главное: WHERE отрабатывает на шаге 3, до группировки. HAVING — на шаге 5, после. Из этого вытекает всё практическое поведение.

WHERE — фильтр до группировки

WHERE отсекает строки до того, как GROUP BY начнёт что-либо группировать. Внутри WHERE нельзя использовать агрегаты — потому что агрегатов на этом этапе ещё не существует.

Считаем число доставленных заказов у каждого клиента — фильтр по статусу в WHERE:

PostgreSQL

Сначала WHERE status = 'delivered' оставил только delivered-заказы. Потом GROUP BY customer_id разрезал на группы. Потом COUNT(*) посчитал, сколько delivered-заказов у каждого клиента.

Если попытаться вставить агрегат в WHERE, будет ошибка:

Ошибка: aggregate functions are not allowed in WHERE:

PostgreSQL

PostgreSQL вернёт aggregate functions are not allowed in WHERE. Это логично: на момент обработки WHERE групп ещё нет, считать COUNT(*) не от чего.

HAVING — фильтр после группировки

HAVING работает на уровне групп, после того как агрегаты уже посчитаны. Внутри HAVING агрегаты разрешены — это и есть его главное предназначение.

Клиенты, у которых больше одного заказа любого статуса:

PostgreSQL

Здесь HAVING COUNT(*) > 1 оставляет только те группы, где счётчик больше единицы. Группы с одним заказом отфильтрованы из результата.

Можно ли в HAVING писать не-агрегаты?

Технически да, но только колонки, входящие в GROUP BY. Это эквивалентно WHERE, и почти всегда стоит написать в WHERE — будет понятнее и эффективнее.

Два эквивалентных запроса. Второй — анти-паттерн, читается хуже:

PostgreSQL

Правило стиля: фильтр на сырые колонки — в WHERE. Фильтр на агрегаты — в HAVING. Это правило не оптимизация (оптимизатор всё равно push-down-ит фильтры), а правило ясности кода: читающий сразу видит, что является «отсечкой строк», а что «отсечкой групп».

Часто встречающиеся комбинации

Реальный запрос обычно использует оба:

Клиенты с минимум 2 доставленными заказами:

PostgreSQL

Сначала WHERE status = 'delivered' оставил только delivered-заказы. Потом GROUP BY c.id разрезал на клиентов. Потом HAVING COUNT(*) >= 2 оставил только тех, у кого таких заказов 2 или больше.

HAVING без GROUP BY: вся таблица как одна группа

Что произойдёт, если написать HAVING без GROUP BY? Это легально — и означает «считай всю таблицу одной группой».

HAVING без GROUP BY: вернёт строку, только если условие на всю таблицу истинно:

PostgreSQL

Если в orders 20 заказов — запрос вернёт total_orders = 20. Если бы было 5 — вернул бы пустой результат (ноль строк), потому что условие 5 > 10 ложно для единственной «группы».

Сравни с похожим запросом без HAVING:

Без HAVING: всегда возвращает строку с числом:

PostgreSQL

Этот всегда вернёт ровно одну строку — даже если таблица пустая (тогда COUNT(*) = 0). А запрос выше с HAVING COUNT(*) > 10 может вернуть ноль строк. Это разница между «есть результат, но он 0» и «результата нет».

HAVING vs подзапрос — два способа фильтрации по агрегату

Иногда хочется написать «клиенты, у которых > 2 заказов» через подзапрос. Это работает, но обычно длиннее:

Эквивалент через подзапрос — работает, но многословно:

PostgreSQL

Внутри подзапроса — обычный GROUP BY без HAVING. Снаружи — обычный WHERE по уже посчитанной колонке. Результат тот же, но кода больше. HAVING — это синтаксический сахар, чтобы избежать такой обёртки.

Алиас из SELECT в HAVING — нельзя в стандарте, но можно в Postgres

Из-за логического порядка SELECT идёт после HAVING, поэтому стандарт запрещает использовать алиасы из SELECT в HAVING. PostgreSQL позволяет — это его расширение:

В Postgres можно ссылаться на алиас из SELECT в HAVING (расширение):

PostgreSQL

Совет: если пишешь портируемый SQL, повторяй агрегат в HAVING явно — HAVING COUNT(*) > 1. Это работает везде.

Проверка знанийKnowledge check
В чём разница между WHERE status = 'delivered' AND COUNT(*) > 2 и HAVING status = 'delivered' AND COUNT(*) > 2 при использовании с GROUP BY customer_id? Какой из этих фрагментов синтаксически валиден, какой нет, какие у них последствия?
ОтветAnswer
Первый невалиден — в WHERE нельзя писать COUNT(*), потому что WHERE отрабатывает до группировки и агрегатов ещё нет. Будет ошибка парсера. Второй валиден синтаксически, но фактически бесполезен или ошибочен: status — не group-by-колонка, поэтому HAVING status = 'delivered' либо упадёт, либо потребует, чтобы status был в GROUP BY. Правильное разбиение: WHERE status = 'delivered' (фильтр до группировки) + HAVING COUNT(*) > 2 (фильтр после). Каждое условие в свою стадию.
WHERE и HAVING в пайплайне исполнения

Чек-лист

  • WHERE отрабатывает до GROUP BY — на сырых кортежах. Агрегаты внутри запрещены.
  • HAVING отрабатывает после GROUP BY — на группах. Агрегаты внутри разрешены и ожидаемы.
  • Фильтр на сырую колонку → WHERE. Фильтр на агрегат → HAVING. Это правило стиля.
  • HAVING без GROUP BY трактует всю таблицу как одну группу — может вернуть либо одну строку, либо ноль строк.
  • HAVING эквивалентен подзапросу WHERE поверх агрегации, просто короче.
  • В PostgreSQL можно использовать алиасы из SELECT в HAVING (расширение). В переносимом коде лучше повторять агрегат явно.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6