Введение
Агрегатные функции вычисляют единое значение на основе множества строк — COUNT (количество), SUM (сумма), AVG (среднее), MIN/MAX (минимум/максимум) — в статье разберём их использование как для всей таблицы целиком, так и для отдельных групп строк через GROUP BY, и фильтрацию уже агрегированных результатов через HAVING.
Концепция
Без GROUP BY агрегатная функция применяется ко всем строкам, отобранным условием WHERE (если оно есть), давая единственное итоговое значение. С GROUP BY строки разбиваются на группы по значению указанного столбца (или нескольких столбцов), и агрегатная функция вычисляется отдельно для каждой такой группы, давая одну итоговую строку результата на группу. HAVING — аналог WHERE, но применяемый после группировки, к уже агрегированным значениям (поскольку WHERE фильтрует строки до группировки, оно не может ссылаться на результат агрегатной функции).
Пример кода: Агрегатные функции
-- Простая агрегация без группировки — одно итоговое значение для всей таблицы
SELECT COUNT(*) AS total_orders, SUM(total_amount) AS total_revenue, AVG(total_amount) AS avg_order
FROM orders;
-- Группировка — отдельный результат для каждого клиента
SELECT customer_id, COUNT(*) AS order_count, SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
ORDER BY total_spent DESC;
-- HAVING — фильтрация ПОСЛЕ группировки, по агрегированному значению
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(total_amount) > 5000 -- только клиенты, потратившие больше 5000 суммарно
ORDER BY total_spent DESC;
-- WHERE и HAVING вместе — фильтрация строк ДО группировки и групп ПОСЛЕ
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2026-01-01' -- фильтрация отдельных заказов до группировки
GROUP BY customer_id
HAVING COUNT(*) >= 3 -- фильтрация клиентов с 3+ заказами после группировки
ORDER BY order_count DESC;
Пояснения к коду
Различие WHERE order_date >= '2026-01-01' (применяется к отдельным строкам заказов до того, как они объединяются в группы по клиенту) и HAVING COUNT(*) >= 3 (применяется к уже посчитанному количеству заказов в каждой группе) демонстрирует фундаментальный порядок выполнения логических этапов SQL-запроса: сначала фильтрация строк (WHERE), затем группировка (GROUP BY) и вычисление агрегатов, и только потом фильтрация уже агрегированных групп (HAVING) — попытка использовать WHERE COUNT(*) >= 3 была бы синтаксической ошибкой, поскольку на этапе применения WHERE агрегированное значение COUNT(*) для группы ещё не существует.
Подводные камни
- Попытка использовать агрегатную функцию в
WHEREвместоHAVING— это прямая синтаксическая ошибка в большинстве СУБД (WHEREвыполняется до агрегации и не имеет доступа к её результатам), и понимание правильного логического порядка выполнения запроса (FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY, хотя синтаксический порядок написания запроса отличается от этого логического порядка выполнения) важно для правильного выбора междуWHEREиHAVING. - Выбор столбцов в
SELECT, не входящих ни вGROUP BY, ни в агрегатную функцию — большинство строгих СУБД (PostgreSQL по умолчанию) требуют, чтобы каждый столбец вSELECTпри наличииGROUP BYбыл либо частью группировки, либо обёрнут в агрегатную функцию, поскольку для столбца, не входящего ни в то, ни в другое, неоднозначно, какое из множества значений внутри группы должно быть показано в результате; MySQL исторически был менее строг в этом отношении (мог «тихо» выбрать произвольное значение), что было источником путаницы при переносе запросов между разными СУБД. - Игнорирование того, что
COUNT(*)иCOUNT(столбец)могут давать разные результаты при наличии NULL значений —COUNT(*)считает все строки независимо от их содержимого, тогда какCOUNT(конкретный_столбец)считает только строки, где это конкретное значение столбца НЕ NULL; использование неверного варианта может привести к неожиданно заниженному количеству при наличии NULL значений в столбце, переданном вCOUNT(). - Деление на ноль или на NULL при вычислении производных метрик из агрегатов (например,
total_revenue / order_countдля средней суммы заказа, при отсутствии заказов вообще для какой-то группы) — для случаев, где знаменатель агрегации потенциально может быть нулём (что дляCOUNT()группы, существующей в результатеGROUP BY, на практике невозможно, поскольку группа не появилась бы без хотя бы одной строки, но возможно для других видов агрегатных вычислений), стоит явно защититься черезCASE WHENили функции типаNULLIF()/COALESCE()(статья 396), предотвращающие ошибку деления на ноль.