ПрограммированиеBackend разработчик

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

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

Ответ

Вычисление кумулятивных итогов и бегущих сумм традиционно в SQL решалось через оконные функции (например, SUM() OVER(ORDER BY ...)), однако в ранних или упрощённых версиях СУБД доступны лишь подзапросы и группировки. Исторически архитекторы БД искали обходные пути до появления стандарта SQL:2003 с поддержкой оконных функций.

Проблема — при отсутствии оконных функций для каждой строки необходимо в явном виде вычислять сумму всех предыдущих значений, что приводит к O(N^2) вложенным запросам при достаточно больших выборках, если не применить хитрости.

Решение:

Обычно используют коррелированные подзапросы или временные таблицы с обновлением значений:

Пример кода:

-- Кумулятивная сумма коррелированным запросом SELECT t1.id, t1.amount, ( SELECT SUM(t2.amount) FROM transactions t2 WHERE t2.id <= t1.id ) AS running_total FROM transactions t1 ORDER BY t1.id; -- Через временную таблицу с ручным обновлением значения CREATE TEMPORARY TABLE temp_running (id INT, amount INT, running_total INT); -- Проходим по строкам средствами внешнего кода (например, pl/pgsql), последовательно добавляя сумму

Ключевые особенности:

  • Метод работает только если есть уникальный критерий сортировки (id, произведенная дата)
  • Коррелированный подзапрос масштабируется плохо — экспоненциальный рост времени выполнения
  • Для больших объёмов данных логичнее использовать ETL с агрегацией вне SQL или средствами процедур

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

Обеспечивает ли гарантированную сортировку ORDER BY в коррелированном подзапросе?

Нет — subquery не обязательно влияет на сам результат. Сортировка итоговой выборки всегда задается наружно в основном запросе: итог зависит только от фильтрации по WHERE.

Можно ли параллелить вычисление кумулятивной суммы в таком подходе?

Нет — последовательность очень важна, особенно при вычислениях в зависимости от предшествующих строк, из-за чего простое распараллеливание невозможно в обычном SQL.

Почему коррелированный подзапрос столь медленный при большом количестве строк?

Потому что для каждой строки заново вычисляется сумма по набору предыдущих строк. Это приводит к O(N^2) операциями. На образце 100 тыс. строк это уже может занять минуты или даже часы.

Типовые ошибки и анти-паттерны

  • Неправильная фильтрация по id вместо фактической даты — суммы "скачут" на дыры в id
  • Попытка выполнять суммирование без упорядочивания данных
  • Использование такого подхода для огромных таблиц, когда нужен ETL или партиционированная обработка

Пример из жизни

Негативный кейс

Аналитик посчитал дневную кумулятивную выручку по дате через коррелированный подзапрос, а в таблице периодически появлялись удалённые id (дыры). Итоговая сумма имела скачкообразные провалы и зависела не от даты, а от порядка id.

Плюсы:

  • Работает для малых выборок, не требуется оконных функций

Минусы:

  • Данные некорректны, считает не так, как ожидается
  • Сложная поддержка

Позитивный кейс

Инженер вынес обработку кумулятивной суммы в скрипт ETL (Python/pandas), затем залил итоговые значения в отдельную таблицу, синхронизируя только новинки. Итоги всегда согласованы по дате, код работает быстро и с миллионами записей.

Плюсы:

  • Надёжность, возможность дорасчёта без времени простоя
  • Поддержка больших объёмов

Минусы:

  • Сложнее ландшафт — нужны внешние инструменты обработки