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_id | gross_kopecks | running_kopecks |
|---|---|---|
| 201 | 50000 | 50000 |
| 203 | 25000 | 75000 |
| 205 | 20000 | 95000 |
| 206 | 30000 | 125000 |
На последней строке получается 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_id | default_total | rows_total |
|---|---|---|
| 1 | 300 | 100 |
| 2 | 300 | 300 |
| 3 | 600 | 600 |
Первый вариант сразу показывает 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 с итоговой работой и возможностью получить сертификат.