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 из 0
Концептуальный
Вопрос 1 из 4. В каком порядке логически обрабатываются предложения запроса SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY?

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

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

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

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