Что вернёт агрегатный запрос без GROUP BY, если после фильтрации не осталось строк, и чем объясняется различие между COUNT и SUM?
Без GROUP BY агрегатный запрос обычно формирует одну итоговую группу даже при пустом входном наборе. Для неё COUNT возвращает 0, а SUM возвращает NULL, потому что количество строк равно нулю, тогда как сумма отсутствующих значений не имеет определённого числового результата.
Агрегаты SQL предназначены для свёртки набора строк в одно значение: количество, сумму, минимум, максимум или среднее. Правила для пустого набора должны позволять отчётам сохранять форму результата даже тогда, когда исходных строк нет.
Поэтому агрегатный запрос без GROUP BY рассматривает весь результат фильтрации как одну группу. Для разных агрегатов SQL определяет разные нейтральные или неопределённые результаты: у COUNT это ноль, а у большинства остальных агрегатов — NULL.
Предположим, отчёт запрашивает продажи за период, в котором заказов нет. Если разработчик ожидает числовой ноль от любого агрегата, он может получить NULL и ошибку при последующих вычислениях, например при сложении или расчёте доли.
Важно отличать пустой набор от группы, содержащей строки, но только с NULL в агрегируемом столбце. В обоих случаях SUM может вернуть NULL, однако причины различаются: в первом случае нет строк вообще, во втором — есть строки, но нет ненулевых значений для суммирования.
Здесь результатом будет одна строка: row_count и amount_count равны 0, total_amount равен NULL, а total_amount_as_zero — 0. COUNT(*) считает строки, а COUNT(amount) — только строки, где amount не равен NULL.
Агрегат без GROUP BY получает один набор входных строк. Даже если после WHERE этот набор пуст, запрос сохраняет одну агрегатную группу, поэтому COUNT может вернуть корректное количество — ноль.
SUM, AVG, MIN и MAX не имеют обычного числового результата для пустого набора. SQL возвращает NULL, который означает отсутствие известного значения, а не число ноль. Для AVG это особенно важно: среднее нельзя вычислить при отсутствии учитываемых значений.
COUNT(*) считает все строки, включая строки, полностью состоящие из NULL. COUNT(column) игнорирует NULL в указанном столбце. Поэтому при наличии строк, но при NULL во всех значениях столбца, первый счётчик может быть положительным, второй — нулевым, а SUM — NULL.
Если бизнес-смысл требует именно нуля, его следует задать явно через COALESCE. Это не меняет поведение агрегата, а преобразует отсутствие результата в значение, принятое в конкретном отчёте.
Нужно учитывать и HAVING: если агрегатное условие ложно для единственной пустой группы, итоговый запрос может не вернуть ни одной строки. Значит, «агрегат возвращает NULL» и «запрос возвращает ноль строк» — разные ситуации.
В отчёте по биллингу требовалось показать сумму платежей за выбранный месяц. В месяце без платежей SUM(amount) вернул NULL, и внешний сервис отобразил пустое поле вместо ожидаемого нуля.
Рассматривались два варианта. Можно было обработать NULL в приложении, но тогда одинаковое правило пришлось бы дублировать в разных потребителях отчёта. Другой вариант — преобразовать результат в SQL через COALESCE, сохранив семантику данных рядом с вычислением.
Выбран второй вариант: сумма оставалась NULL на уровне базового агрегата, но в прикладном отчёте явно преобразовывалась в ноль. Это позволило отличать отсутствие значения в универсальных запросах от бизнес-правила конкретного отчёта и устранило неоднозначное отображение пустого месяца.
Дополнительный вопрос: чем отличается пустой входной набор от строк, в которых агрегируемый столбец содержит только NULL?
При пустом входе нет строк: COUNT(*) и COUNT(column) возвращают 0, а SUM(column) — NULL. Если строки есть, но столбец во всех них NULL, COUNT(*) будет положительным, COUNT(column) — 0, а SUM(column) обычно также останется NULL.
Это различие важно при диагностике данных: нулевой COUNT(column) не доказывает отсутствие строк, он может означать отсутствие заполненных значений.
Дополнительный вопрос: почему замена SUM на COALESCE(SUM(...), 0) может быть опасна в аналитике?
Она стирает различие между отсутствием наблюдений и фактической нулевой суммой. Для финансового отчёта это может быть приемлемо, если по правилам бизнеса отсутствие платежей трактуется как ноль, но для контроля качества данных такая замена способна скрыть проблему загрузки.
Поэтому COALESCE следует применять на границе представления или отчёта, где такое преобразование действительно является частью бизнес-смысла.
Дополнительный вопрос: может ли агрегатный запрос без GROUP BY вернуть ноль строк?
Да. Сам агрегат без GROUP BY обычно создаёт одну группу, но HAVING фильтрует уже агрегированный результат. Если условие HAVING ложно или имеет значение FALSE для этой группы, строка удаляется.
Следовательно, запрос без GROUP BY может дать либо одну строку с агрегатными значениями, включая NULL, либо не дать строк вообще — в зависимости от наличия и результата HAVING.