Tier 5 · Expert

Running Totals and Cumulative Metrics in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

What are Running Totals in SQL?

A running total accumulates a metric across an ordered sequence of rows so that each row carries the sum of all preceding rows plus its own value. Cumulative revenue, cumulative signups, cumulative events — all follow the same two-stage pattern: aggregate by period first, then accumulate across periods with a window function.

Your manager wants cumulative revenue for the year, showing how total revenue grows each day. The raw orders table has one row per order, not one per day. You need to aggregate to day first, then accumulate. Running the window function directly on the unaggregated table gives you a running total per transaction — one row per order, each carrying the sum of all prior orders. That answers a different question.

WITH daily_revenue AS (
    SELECT
        ordered_at::date AS order_date,
        SUM(total_amount) AS revenue
    FROM orders
    GROUP BY ordered_at::date
)
SELECT
    order_date,
    revenue,
    SUM(revenue) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_revenue
FROM daily_revenue
ORDER BY order_date
WITH daily AS (
  SELECT ordered_at::date AS order_date, SUM(total_amount) AS revenue
  FROM orders GROUP BY ordered_at::date
)
SELECT order_date, revenue,
  SUM(revenue) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_revenue
FROM daily ORDER BY order_date

The CTE produces one row per day. The window function in the outer query accumulates revenue from the first day through each current day. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW expands the frame by exactly one row at each step — appropriate here because the data has one row per period after aggregation. If the pre-aggregated result ever had two rows with the same date, RANGE would treat them as a single frame boundary and give both rows the same cumulative total, while ROWS would treat them as separate steps. For pre-aggregated data with one row per period, the two behave identically.

Why do you aggregate before computing a running total?

The two-stage structure is deliberate

Applying the window function directly on the unaggregated table produces a running total at the individual transaction level, not the period level. "Cumulative revenue as of each month" requires aggregating to the month first. Skipping the aggregation step produces a running total that increments on every transaction, which answers a different question.

How do you compute a running total per group?

PARTITION BY for per-entity running totals

To compute cumulative revenue per region (not globally), add PARTITION BY:

SUM(revenue) OVER (
    PARTITION BY region
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue_by_region

Without PARTITION BY, the accumulation crosses region boundaries and produces a single global total.

What do gaps in the data do to a running total?

The one thing that trips people up

Gaps in the data affect running totals. If the pre-aggregated data has missing days (no orders on that day, so no row), the running total jumps from the day before the gap to the day after. There's no row for the gap period to show a repeated cumulative total. When the requirement is a continuous series with every day represented, build a date spine first and zero-fill the missing days before running the accumulation.

Practice Running Totals in SQL

Practice · easy ecommerce · Brightlane

Scenario: Brightlane's operations team is tracking how total order volume has grown over time and needs each day's order count alongside the cumulative count from the start of the data through that day.

Task: Write a query to return each order day, the number of orders placed on that day, and the running total of all orders placed from the earliest day through that day.

Assumptions:

  • An order day is identified by its date.
  • A day's daily_orders is the count of orders placed on that day.
  • A day's cumulative_orders is the combined order count from the earliest day in the data through that day inclusive.

Output:

  • One row per order day present in the data.
  • Columns in this order: order_day, daily_orders, cumulative_orders.
  • Sorted by order_day ascending.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution

10 Running Totals practice problems

Write a query to return each order day, the number of orders placed on that day, and the running total of all orders placed from the earliest day through that day.

easy ecommerce

Write a query to return each calendar month in which users signed up, the count of new users that month, and the running total of all users signed up from the earliest signup month through that month.

easy analytics

Write a query to return each order month, the total orders revenue for that month, and the running total of all orders revenue from the earliest month through that month.

easy ecommerce

Write a query to return each (order_month, status) combination, the count of orders with that status in that month, and the running total of orders in that status from the earliest such month through the current month — restarting at the earliest month for each status independently.

medium ecommerce

Write a query to return each signup month, the count of new users that month, and the rolling 3-month total — covering the current month and the two months immediately preceding it.

medium analytics

Write a query to return each order month in which at least one delivered order was placed, the total delivered revenue for that month, and the running total of delivered revenue from the earliest such month through that month.

medium ecommerce

Write a query to return each calendar month in which at least one salary became effective, the total salary amount that became effective in that month, and the running total of all salary commitments from the earliest such month through that month.

medium hr

Write a query to return each order month, its calendar order_year, the revenue for that month, and the ytd_revenue — the running total from the start of that calendar year through that month, restarting at the start of each new year.

hard ecommerce

Write a query to return each order day, the average order value for that day alone, and the running average order value across every individual order placed from the earliest day through that day.

hard ecommerce

Write a query to return each order month, the revenue for that month, the cumulative revenue from the earliest month through that month, and the remaining revenue from that month through the latest month in the data.

hard ecommerce

Start learning to practice all 10 Running Totals problems, with instant grading and mastery tracking.

Common questions about Running Totals

Why does my running total jump instead of increasing one row at a time?

Because of ties. With ORDER BY and no frame, a window defaults to a RANGE frame, which treats rows sharing the same ORDER BY value as one step and adds them all at once. Two rows on the same date both show the combined total. Add ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW to accumulate strictly row by row.

How do I restart a running total for each customer?

Add PARTITION BY customer_id inside the OVER clause. The sum starts again from zero at every partition boundary, so each customer gets their own running total.

Is a running total the same as a cumulative sum?

Yes. They are two names for one thing: each row carries the total of its own value plus every row before it in the chosen order.

Can I calculate a running total without a window function?

Yes, with a correlated subquery that sums every earlier row, but it has to reconsider the earlier rows once for each row in the result. A window function computes the whole column in a single ordered pass, which is why it is the standard answer.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.