Для отчёта по группам продаж определите, где в логическом порядке SELECT должен применяться фильтр по агрегированной сумме.
Фильтр по агрегированной сумме должен применяться на этапе HAVING, после формирования групп и вычисления агрегата. WHERE работает раньше, на отдельных исходных строках, поэтому обычно не может фильтровать результат SUM, COUNT или другого агрегатного выражения.
SQL использует декларативную модель: разработчик описывает требуемый результат, а не пошаговый алгоритм его получения. Для обработки групповых результатов язык разделяет фильтрацию исходных строк (WHERE) и фильтрацию уже сформированных групп (HAVING).
Такое разделение решает проблему разных уровней данных: строка существует до группировки, а сумма или количество появляются только после объединения строк в группы.
Предположим, отчёту нужны только те клиенты, чья суммарная покупка превышает заданный порог. На этапе проверки отдельной строки такой суммы ещё нет, поэтому попытка применить агрегатный фильтр в WHERE приводит к ошибке или к неверной логике в зависимости от конкретного выражения и диалекта.
Неверное размещение обычного фильтра тоже меняет результат. Если условие по статусу заказа поставить до группировки, исключённые заказы не попадут в сумму; если поставить его после группировки, сумма сначала будет рассчитана по всем заказам.
Логический порядок обработки типичного группового запроса выглядит так:
Минимальный пример:
Здесь WHERE сначала оставляет только завершённые продажи. Затем они группируются по клиенту, вычисляется сумма, и HAVING оставляет только группы с суммой больше 10 000.
Условия по исходным столбцам следует помещать в WHERE, если они должны влиять на состав суммируемых строк. Условия по агрегатам помещают в HAVING. Иногда диалекты допускают ссылку на псевдоним агрегата в HAVING, но переносимость такого решения хуже, чем у повторения агрегатного выражения.
Важно учитывать NULL. SUM игнорирует значения NULL, а если в группе нет ни одного ненулевого значения, результат может быть NULL; сравнение NULL > 10000 не является истинным, поэтому такая группа не пройдёт фильтр. Для явного трактования отсутствующей суммы как нуля применяют COALESCE, если это соответствует бизнес-смыслу.
Оптимизатор может физически выполнить операции в другом порядке, например использовать индексы или предварительно протолкнуть безопасный предикат ниже группировки. Это не меняет логическую семантику запроса: результат должен соответствовать описанному логическому порядку.
В отчёте требовалось показать клиентов, у которых завершённые заказы за период дали выручку свыше 100 000. Рассматривались два варианта: отфильтровать статус и период в WHERE, а порог суммы — в HAVING, либо сначала агрегировать все заказы, а затем попытаться исключить незавершённые заказы.
Первый вариант корректен: статус и период определяют, какие строки участвуют в выручке, а порог определяет, какие группы остаются. Второй вариант смешивает состав исходных данных с условием отбора групп и может завысить сумму за счёт заказов, которые не должны учитываться.
Был выбран первый вариант. Он одновременно сохраняет правильную бизнес-семантику и обычно позволяет оптимизатору раньше сократить объём данных по индексируемым условиям периода или статуса.
1. Дополнительный вопрос: Можно ли перенести условие по неагрегированному столбцу из WHERE в HAVING без изменения результата?
Ответ: Иногда результат совпадёт, если столбец входит в группировку и условие проверяется для каждой сформированной группы. Однако это не универсальная замена: HAVING выполняется позже, может обрабатывать уже созданные группы и часто требует большего объёма промежуточных данных. Кроме того, смысл запроса становится менее ясным: фильтр строк следует оставлять в WHERE, а фильтр групп — в HAVING.
2. Дополнительный вопрос: Что означает HAVING без GROUP BY в запросе с агрегатом?
Ответ: В распространённой SQL-семантике весь набор строк рассматривается как одна группа. Тогда условие HAVING либо оставляет единственный агрегированный результат, либо отбрасывает его целиком. Это отличается от WHERE, который может удалить отдельные строки до вычисления агрегата.
Например, проверка общей суммы через HAVING возвращает одну строку только при выполнении условия. Если исходный набор пуст, детали результата зависят от используемого агрегата и диалекта, поэтому поведение COUNT, SUM и других агрегатов нужно учитывать отдельно.
3. Дополнительный вопрос: Почему перенос условия из WHERE в HAVING может изменить значение SUM, даже если итоговый список клиентов выглядит похожим?
Ответ: Потому что условия применяются к разным объектам. WHERE удаляет строки до группировки, поэтому исключённые строки не участвуют в SUM; HAVING удаляет уже готовые группы, не изменяя рассчитанную внутри них сумму.
Например, фильтрация только завершённых заказов в WHERE даёт сумму завершённых заказов. Если же сначала посчитать все заказы, а затем оставить клиентов с подходящей группой в HAVING, сумма будет включать и незавершённые заказы. Совпадение списка клиентов не гарантирует совпадение агрегированных показателей.