Что на самом деле делает агрегат
Большинство SQL-функций, с которыми вы уже сталкивались, работают построчно: UPPER(name) отрабатывает один раз на строку, ROUND(price, 2) - то же самое. Агрегатные функции устроены иначе. Они смотрят на набор строк целиком и сворачивают его в одно значение.
Заведём небольшую таблицу для опытов:
На входе пять строк, на выходе - одна. Вот и вся ментальная модель: агрегаты сжимают строки в сводку. Без GROUP BY сводка покрывает все строки результата.
COUNT: строки против значений
У COUNT есть три формы, и разница между ними заметна:
COUNT(*)считает строки.NULL-ы тоже. Всегда возвращает число.COUNT(column)считает не-NULL значения в этом столбце.COUNT(DISTINCT column)считает уникальные не-NULL значения.
Пять строк, у трёх есть amount, трое разных клиентов. Если COUNT(amount) оказался меньше COUNT(*) - вот почему: NULL-ы не считаются.
SUM, AVG, MIN, MAX
Арифметические агрегаты ведут себя ожидаемо, с одним негромким правилом - все они пропускают NULL:
AVG тут - это (10 + 20 + 30) / 3 = 20.0, а не 60 / 4 = 15.0. Знаменатель - количество не-NULL значений. Если хочется, чтобы пропуски считались как ноль, скажите об этом явно:
MIN и MAX работают и со строками, и с датами: для текста - лексикографическое сравнение, для дат в стандартном ISO-формате - как с обычными ISO-строками.
SUM против TOTAL
В SQLite есть второй «суммирующий» агрегат - TOTAL, - который снимает пару раздражающих особенностей SUM:
SUMот нуля строк возвращаетNULL.TOTAL-0.0.SUMот сплошныхNULL-ов возвращаетNULL.TOTAL-0.0.TOTALвсегда возвращает число с плавающей точкой и не страдает целочисленным переполнением.
Платой идёт нестандартность: TOTAL - это расширение SQLite, и результат всегда REAL, что может неожиданно прилететь, если вы рассчитывали на целое. Берите его, когда «нет строк - значит ноль» - это правильный ответ для вашего приложения, и оставайтесь с SUM, когда важно стандартное поведение.
DISTINCT внутри агрегатов
DISTINCT можно поставить внутрь любого агрегата, не только COUNT. Он убирает дубликаты значений до агрегации:
SUM(amount) складывает суммы всех строк. SUM(DISTINCT amount) берёт каждую уникальную сумму один раз - может быть полезно для чего-то вроде «общая сумма уникальных счетов», но в обычной работе нужно редко. По-настоящему распространённый вариант - COUNT(DISTINCT customer).
FILTER: агрегация по подмножеству
Когда нужно агрегировать только часть строк, первое, что приходит в голову, - WHERE. Но WHERE фильтрует всё подряд: «посчитать оплаченные заказы» и «посчитать возвраты» в одном запросе так не сделать. Эту задачу решает FILTER:
Каждое FILTER (WHERE ...) действует только на свой агрегат. Один проход по таблице - несколько срезов сразу. До появления FILTER ту же задачу решали через SUM(CASE WHEN status = 'paid' THEN amount END) - идея та же, но многословнее.
GROUP_CONCAT: склеить значения в строку
GROUP_CONCAT - белая ворона среди агрегатов. Вместо числа он возвращает значения, склеенные в одну строку:
Разделитель по умолчанию - запятая. Передайте второй аргумент, чтобы поставить что-то другое. Порядок не гарантирован, если только вы не пишете GROUP_CONCAT(tag ORDER BY tag) - пригодится, когда вывод появляется в интерфейсе и должен быть стабильным.
Агрегация без GROUP BY
Каждый пример выше с агрегатами без GROUP BY возвращал ровно одну строку. Это и есть правило: SELECT с агрегатами и без GROUP BY - это сводка по всей таблице (после WHERE), укладывающаяся в одну строку.
Агрегаты в одном запросе можно смешивать:
А вот чего делать не стоит - это смешивать неагрегированные столбцы с агрегатами и ждать осмысленного результата:
-- SQLite разрешит, но значение `customer` будет произвольным.
SELECT customer, SUM(amount) FROM orders;
SQLite ошибки тут не выдаст (другие СУБД - выдадут), но просто наугад покажет имя одного из клиентов рядом с общей суммой. Если нужна сумма по каждому клиенту - нужен GROUP BY, и про него - следующая страница.
Дальше: GROUP BY и HAVING
Агрегаты по всей таблице отвечают на «сколько в сумме». Агрегаты по группам - по клиенту, по месяцу, по статусу - отвечают на куда более интересные вопросы. GROUP BY - это про то, как разложить строки по корзинам перед агрегацией, а HAVING - как отфильтровать сами группы по результату агрегата. Об этом - дальше.
Часто задаваемые вопросы
Что такое агрегатные функции в SQLite?
Агрегатные функции принимают много строк и возвращают одно итоговое значение. Встроенные - это COUNT, SUM, AVG, MIN, MAX, TOTAL и GROUP_CONCAT. Без GROUP BY они сворачивают весь результирующий набор в одну строку.
В чём разница между SUM и TOTAL в SQLite?
Обе складывают числа, но SUM возвращает NULL, когда все входные значения - NULL, и по возможности использует целочисленную арифметику (которая может переполниться). TOTAL всегда возвращает число с плавающей точкой и 0.0, если строк нет вовсе. Берите TOTAL, когда нужен гарантированно числовой результат, и SUM, когда важно поведение по стандарту SQL.
Как посчитать количество уникальных значений в SQLite?
Поставьте DISTINCT внутрь вызова: COUNT(DISTINCT customer_id). Это посчитает уникальные значения, не считая NULL. Просто COUNT(column) считает не-NULL значения, включая дубликаты, а COUNT(*) - все строки целиком, независимо от NULL.
Игнорируют ли агрегаты SQLite значения NULL?
Да - каждый агрегат, кроме COUNT(*), пропускает NULL на входе. AVG делит на количество не-NULL значений, а не на общее число строк. Исключение - COUNT(*): он считает строки, а не значения, поэтому NULL-ы тоже попадают в счёт.