Собесов

Альфа-Банк SQL — нарастающий итог операций по месяцу

SQLОконные функцииСредняяMiddle

Условие

Таблица #oper(date date, cnt int) хранит количество операций на каждую календарную дату. Нужно вывести на каждую дату количество операций с начала месяца по указанную дату включительно нарастающим итогом.

Тестовые данные

insert into #oper values
('2019-06-02', 1985), ('2019-06-03', 1577), ('2019-06-04', 1597),
('2019-06-05', 1468), ('2019-07-06', 82),   ('2019-07-08', 1689),
('2019-07-09', 1556), ('2019-07-10', 1480), ('2019-07-11', 1405),
('2019-07-12', 1502);

Решение

Подход

Классическая оконная функция с PARTITION BY по месяцу. Месяц считается границей рестарта суммы.

Реализация

SELECT
  date,
  cnt,
  SUM(cnt) OVER (
    PARTITION BY DATE_TRUNC('month', date)
    ORDER BY date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cnt_running
FROM #oper
ORDER BY date;

В MS SQL Server вместо DATE_TRUNC используется DATEFROMPARTS(YEAR(date), MONTH(date), 1) или EOMONTH(date, -1).

Ожидаемый результат

date cnt cnt_running
2019-06-02 1985 1985
2019-06-03 1577 3562
2019-06-04 1597 5159
2019-06-05 1468 6627
2019-07-06 82 82
2019-07-08 1689 1771
2019-07-09 1556 3327
2019-07-10 1480 4807
2019-07-11 1405 6212
2019-07-12 1502 7714

Подводные камни

  1. Без PARTITION BY по месяцу сумма не сбрасывается на 1 июля — типичная ошибка.
  2. ORDER BY без ROWS BETWEEN в большинстве СУБД эквивалентен RANGE UNBOUNDED PRECEDING AND CURRENT ROW. Если есть дубликаты дат — RANGE суммирует все одинаковые даты в одну точку, что может дать неожиданный результат. Безопаснее явно указывать ROWS.
  3. Пропущенные даты. Если в задании требуется заполнить пропуски нулями, нужен cross join со справочником дат.
  4. Часовой пояс. Если date — это timestamp, придётся учитывать TZ при расчёте месяца.

Альтернатива

Через подзапрос-самоjoin:

SELECT
  o1.date, o1.cnt,
  (SELECT SUM(o2.cnt)
   FROM #oper o2
   WHERE YEAR(o2.date) = YEAR(o1.date)
     AND MONTH(o2.date) = MONTH(o1.date)
     AND o2.date <= o1.date) AS cnt_running
FROM #oper o1
ORDER BY o1.date;

Менее эффективно (O(N²)) и менее читаемо.

Эталонный ответ

SUM(cnt) OVER (PARTITION BY DATE_TRUNC('month', date) ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) — нарастающий итог в пределах месяца.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти