ПрограммированиеSQL аналитик

Как реализовать эффективную агрегацию с послойной фильтрацией с помощью оконных функций и группировок в SQL? В чём отличия подходов, и какие ошибки часто допускают разработчики?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

Чтобы эффективно строить агрегации с фильтрацией по "уровням" (например, сначала фильтруем, потом считаем суммы по группам, затем по всему набору), используют оконные функции (OVER()) и вложенные GROUP BY.

  • Группировка через GROUP BY агрегирует только по выбранным полям; исключить влияние "посторонних" строк можно только через предварительный фильтр (WHERE).
  • Оконные функции позволяют посчитать агрегаты по определённым фрагментам данных, накладывая фильтры на результирующую выборку (например, только среди строк той же группы).

Пример: Найдём максимальную сумму заказа по каждому менеджеру, но только среди заказов со статусом 'paid', и затем выведем имя менеджера с абсолютным максимумом среди всех.

WITH PaidOrders AS ( SELECT ManagerID, SUM(OrderAmount) AS TotalPaid FROM Orders WHERE Status = 'paid' GROUP BY ManagerID ), WithMax AS ( SELECT *, MAX(TotalPaid) OVER() AS MaxTotalPaid FROM PaidOrders ) SELECT ManagerID, TotalPaid FROM WithMax WHERE TotalPaid = MaxTotalPaid;

Такой подход (CTE + оконные функции) позволяет реализовать многоуровневую фильтрацию и агрегацию.

Вопрос с подвохом.

Подвох: "Чем отличается выполнение фильтра WHERE перед группировкой (GROUP BY) от применения HAVING после? Чем это часто оборачивается при отчетах?"

Ответ: WHERE отбрасывает строки ещё до группировки, то есть даёт строгий входной набор. HAVING фильтрует агрегированные группы — поэтому может временно "оставить" лишние строки, если фильтр логически не согласован. Неправильное место фильтра часто приводит к ошибке в итоговых агрегатах или к неверным результатам отчётов.

-- Получаем сумму только по 'paid', через WHERE SELECT ManagerID, SUM(OrderAmount) FROM Orders WHERE Status = 'paid' GROUP BY ManagerID; -- Или считаем суммы по всем, а потом отрезаем HAVING SELECT ManagerID, SUM(OrderAmount) FROM Orders GROUP BY ManagerID HAVING SUM(OrderAmount) > 1000;

Примеры реальных ошибок из-за незнания тонкостей темы.


История

Проект: Отчёты по продажам, поверочный аудит.

Ошибка: Разработчик применил HAVING Status='paid' вместо WHERE, агрегаты ошибочно включали и неоплаченные заказы, из-за чего некорректно рассчитывались годовые KPI персонала.



История

Проект: Банковская аналитика.

Ошибка: К сложной агрегации пытались применить оконную функцию без PARTITION BY, из-за чего агрегаты считались по всей таблице вместо группы. Бюджет департамента просчитан некорректно — пришлось восстанавливать вручную.



История

Проект: Интернет-магазин, статистика заказов.

Ошибка: Вкладывание оконных функций в подзапрос не учли при оптимизации — из-за этого сервер сделал многократную обработку данных, запрос стал работать в 20 раз медленнее, чем просто с двойным GROUP BY.