Как посчитать накопительный итог (running total) в SQL
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Что такое running total
Running total (накопительный итог, cumulative sum) — сумма всех значений до текущей строки включительно.
Пример:
day | revenue | running_total
----------|---------|---------------
2026-04-01| 100 | 100
2026-04-02| 150 | 250
2026-04-03| 200 | 450
2026-04-04| 120 | 5701. Базовый running total
SELECT
day,
revenue,
SUM(revenue) OVER (ORDER BY day) AS running_total
FROM daily_revenue
ORDER BY day;SUM() OVER (ORDER BY ...) без PARTITION BY — накапливает по всем строкам.
2. Running total по группам
Накопительный итог в каждой категории:
SELECT
category,
day,
revenue,
SUM(revenue) OVER (PARTITION BY category ORDER BY day) AS running_total
FROM daily_revenue_by_category
ORDER BY category, day;Сбрасывается на каждую категорию.
3. Running total с явным окном (по умолчанию)
По умолчанию SUM() OVER (ORDER BY ...) использует:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWЭто и есть running total.
Явно:
SELECT
day,
revenue,
SUM(revenue) OVER (
ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM daily_revenue;4. Скользящее окно (moving average)
Среднее за последние 7 дней:
SELECT
day,
revenue,
AVG(revenue) OVER (
ORDER BY day
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM daily_revenue;Включает текущий + 6 предыдущих = 7 дней.
5. Running total с reset каждый месяц
SELECT
day,
revenue,
SUM(revenue) OVER (
PARTITION BY DATE_TRUNC('month', day)
ORDER BY day
) AS monthly_running_total
FROM daily_revenue
ORDER BY day;Сбрасывается в 1-й день каждого месяца.
6. Cumulative users (когортный)
Накопительное число пользователей по датам регистрации:
WITH daily_signups AS (
SELECT
DATE(created_at) AS signup_day,
COUNT(*) AS new_signups
FROM users
GROUP BY 1
)
SELECT
signup_day,
new_signups,
SUM(new_signups) OVER (ORDER BY signup_day) AS total_users_to_date
FROM daily_signups
ORDER BY signup_day;7. Running total с процентом от итогового
WITH daily AS (
SELECT day, revenue
FROM daily_revenue
),
cumulative AS (
SELECT
day,
revenue,
SUM(revenue) OVER (ORDER BY day) AS running_total
FROM daily
),
total AS (
SELECT SUM(revenue) AS grand_total FROM daily
)
SELECT
c.day,
c.revenue,
c.running_total,
100.0 * c.running_total / t.grand_total AS pct_of_total
FROM cumulative c, total t
ORDER BY c.day;8. Running count уникальных
Кол-во уникальных пользователей на каждый день:
-- это сложно через оконку, обычно через подзапрос
SELECT
day,
(SELECT COUNT(DISTINCT user_id)
FROM events e2
WHERE DATE(e2.event_at) <= days.day) AS cumulative_unique_users
FROM (SELECT DISTINCT DATE(event_at) AS day FROM events) days
ORDER BY day;Медленно для больших данных. Альтернатива — HyperLogLog в современных DWH.
9. Running max / min
SELECT
day,
value,
MAX(value) OVER (ORDER BY day) AS running_max,
MIN(value) OVER (ORDER BY day) AS running_min
FROM daily_values;Running max полезен для отслеживания «рекорда» по времени.
10. Running total через self-join (для СУБД без оконок)
MySQL 5.7 и старые версии без оконных функций:
SELECT
a.day,
a.revenue,
SUM(b.revenue) AS running_total
FROM daily_revenue a
JOIN daily_revenue b ON b.day <= a.day
GROUP BY a.day, a.revenue
ORDER BY a.day;Медленно (O(n²)) на больших таблицах, но работает везде.
11. Running total с условием (только paid)
SELECT
day,
revenue,
SUM(CASE WHEN status = 'paid' THEN revenue ELSE 0 END)
OVER (ORDER BY day) AS running_paid_revenue
FROM daily_data;12. Running total и running rate
Для когортного анализа — running retention:
WITH cohort AS (
SELECT user_id, MIN(DATE(event_at)) AS signup_day
FROM events GROUP BY user_id
),
activity AS (
SELECT
c.user_id,
c.signup_day,
DATE(e.event_at) AS activity_day,
e.event_at - c.signup_day::TIMESTAMP AS days_since_signup
FROM cohort c
JOIN events e ON e.user_id = c.user_id
)
SELECT
EXTRACT(DAY FROM days_since_signup)::INT AS day_n,
COUNT(DISTINCT user_id) AS active_at_day_n
FROM activity
GROUP BY day_n
ORDER BY day_n;Частые ошибки
Ошибка 1. Забыть ORDER BY
-- без ORDER BY — непонятный результат
SUM(revenue) OVER (PARTITION BY category)
-- правильно
SUM(revenue) OVER (PARTITION BY category ORDER BY day)Ошибка 2. Неожиданное окно с ORDER BY
Если есть ORDER BY без ROWS — по умолчанию SUM считает RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Это running total (что обычно и нужно).
Но для RANGE при одинаковых значениях в ORDER BY — ВСЕ они включаются. Если не хотите — используйте ROWS:
-- RANGE (default): tie rows объединяются
SUM(x) OVER (ORDER BY day)
-- ROWS: только до текущей строки
SUM(x) OVER (ORDER BY day ROWS UNBOUNDED PRECEDING)Ошибка 3. Путать running и total
-- total (одна цифра для всех)
SUM(revenue) OVER ()
-- running (накопительный)
SUM(revenue) OVER (ORDER BY day)Ошибка 4. Работа без PARTITION, когда нужна
Если хотите running per user — не забудьте PARTITION BY user_id.
Связанные темы
- Оконные функции SQL — шпаргалка
- PARTITION BY — шпаргалка
- Как посчитать процент в SQL
- Задачи на оконные функции
FAQ
Running total или cumulative sum — это одно и то же?
Да, синонимы.
Как добавить сброс по месяцам?
PARTITION BY DATE_TRUNC('month', day). Running total считается отдельно для каждого месяца.
Как сделать running average?
AVG(x) OVER (ORDER BY day). Аналогично SUM, но среднее вместо суммы.
Почему running total не работает в моем MySQL?
Оконные функции в MySQL 8+. В 5.7 и ниже — нет. Используйте self-join или переменные.
Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.