SQL и Qt

Агрегатные функции: COUNT, SUM, AVG, GROUP BY, HAVING

Введение

Агрегатные функции вычисляют единое значение на основе множества строк — 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), предотвращающие ошибку деления на ноль.