Накопительная сумма в SQL: SUM OVER и ловушка одинаковых дат

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

SQL Studio / Разборы SQL / Накопительная сумма в SQL: SUM OVER и ловушка одинаковых дат

Решение: SUM(amount) OVER (ORDER BY date, id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) суммирует текущую и все предыдущие строки. Чтобы результат был воспроизводимым, сортируйте по дате и уникальному ключу, если возможны события с одинаковым временем.

1. Нарастающий итог по доставленным заказам

В таблице orders деньги хранятся в копейках. Уберём отменённые и обрабатываемые заказы, затем посчитаем накопленную сумму.

SELECT order_id, created_at, gross_kopecks,
       SUM(gross_kopecks) OVER (
         ORDER BY created_at, order_id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_kopecks
FROM orders
WHERE status = 'delivered'
ORDER BY created_at, order_id;
Накопленная стоимость доставленных заказов
order_idgross_kopecksrunning_kopecks
2015000050000
2032500075000
2052000095000
20630000125000

На последней строке получается 125 000 копеек, или 1 250 ₽. Это валовая стоимость доставок, не чистые поступления после возвратов.

2. Почему без ROWS суммы иногда «прыгают»

Создадим три заказа: два совершены в один день. В SQLite оконная сумма с ORDER BY day без явной рамки рассматривает одинаковые даты как группу равных значений.

WITH sales(day, order_id, amount) AS (
  VALUES
    ('2026-09-01', 1, 100),
    ('2026-09-01', 2, 200),
    ('2026-09-02', 3, 300)
)
SELECT order_id,
       SUM(amount) OVER (ORDER BY day) AS default_total,
       SUM(amount) OVER (
         ORDER BY day, order_id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS rows_total
FROM sales
ORDER BY day, order_id;
У одинаковых дат результаты различаются
order_iddefault_totalrows_total
1300100
2300300
3600600

Первый вариант сразу показывает 300 для обоих заказов за 1 сентября: это итог группы равных дат. Второй идёт построчно — 100, 300, 600. Если вам нужен итог на каждый заказ, явно задайте ROWS и порядок.

3. Нарастающий итог по дням

Если отчёт строится по дням, правильнее сначала объединить заказы одной даты, затем применить окно. Так каждая строка действительно будет означать один день.

WITH daily AS (
  SELECT date(created_at) AS day,
         SUM(gross_kopecks) AS daily_kopecks
  FROM orders
  WHERE status = 'delivered'
  GROUP BY date(created_at)
)
SELECT day, daily_kopecks,
       SUM(daily_kopecks) OVER (
         ORDER BY day
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_kopecks
FROM daily
ORDER BY day;

На маленькой базе получатся даты 1, 3, 5 и 6 сентября. Дней без доставок в результате нет: если нужен график с непрерывной осью времени, сначала создайте календарь дат и присоедините к нему ежедневные суммы.

Практика

Попробуйте накопительные оплаты по месяцам. В отличие от примера выше, там нарастающий итог считается по подтверждённым платежам.

Смежные материалы: основы оконных функций · средний чек · границы дат.

Официальная справка: SQLite — рамки ROWS, RANGE и группы равных значений.


Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.