Как правильно делать агрегацию в SQL
Что проверяют
Понимаете ли вы логический порядок выполнения запроса и где агрегация тихо даёт неверные цифры. Типичные уточнения: чем WHERE отличается от HAVING, как NULL влияет на AVG и почему после JOIN выросла сумма.
Как отвечать
Начните с порядка: FROM и JOIN, затем WHERE, GROUP BY, HAVING, SELECT вместе с оконными функциями, DISTINCT, ORDER BY, LIMIT. Из него следуют правила:
WHEREпротивHAVING:WHEREфильтрует строки до группировки,HAVINGгруппы после. Условия на обычные поля пишите вWHERE, тогда в группировку попадёт меньше строк;- поля в
SELECT: каждая неагрегированная колонка должна быть вGROUP BYили функционально зависеть от первичного ключа из него. В MySQL начиная с 5.7 по умолчанию включёнONLY_FULL_GROUP_BY, и старые запросы с «лишними» колонками перестают работать; NULL: все агрегаты, кромеCOUNT(*), их пропускают.AVG(col)иAVG(COALESCE(col, 0))дают разные числа, аSUMпо однимNULLвозвращаетNULL, а не 0;- условная агрегация: несколько метрик за один проход через
COUNT(*) FILTER (WHERE status = 'paid')в PostgreSQL илиSUM(CASE WHEN ... THEN 1 ELSE 0 END)в любом диалекте; - детали рядом с итогом: если нужны исходные строки и сумма по группе одновременно, это оконная функция
SUM(amount) OVER (PARTITION BY client_id), а неGROUP BY; - подытоги:
GROUPING SETS,ROLLUPиCUBEдают итоги по нескольким разрезам одним запросом.
Где спотыкаются
Агрегируют после соединения «один ко многим». Заказ с пятью позициями после JOIN превращается в пять строк, и SUM(order_total) умножается на пять. Сначала агрегируйте детальную таблицу в подзапросе или CTE, потом соединяйте.
Считают среднее от средних: средний чек по регионам, усреднённый ещё раз, не равен среднему чеку по всем заказам, потому что у регионов разное число заказов. Нужна взвешенная формула SUM(amount) / COUNT(*) по исходным строкам.
Делят целые на целые. В PostgreSQL COUNT(*) FILTER (...) / COUNT(*) для доли даёт 0, нужно привести одну из частей к numeric или умножить на 1.0.