SQL Running Total — one OVER clause, shown on real orders

Published Aug 2, 2026·Updated Sep 7, 2026·7 min read·Every query runnable in your browser

Skip ahead to practice: solve window function questions in the browser → · or check your SQL interview readiness (10 questions, no signup).

A running total in SQL is SUM(amount) OVER (ORDER BY date_col, id) — the ORDER BY inside OVER makes the sum cumulative, so each row shows its own amount plus every row before it.
SELECT date_col, amount, SUM(amount) OVER (ORDER BY date_col, id) AS running_total FROM t; — no GROUP BY, every row is kept; add PARTITION BY customer_id inside OVER to restart the total per group.

A running total is SUM(x) OVER (ORDER BY ...)adding ORDER BY inside OVER turns a plain sum into a cumulative one. No CUMSUM function exists in SQL; the ordered window is the cumsum. Here it is on real order data, then per-customer, then the two traps: ties and the pre-window self-join you will still meet in old code.

The basic running total

Revenue to date, after each order — real output
SELECT order_date, total,
  ROUND(SUM(total) OVER (ORDER BY order_date, order_id), 2) AS running_total
FROM orders
ORDER BY order_date, order_id;
order_datetotalrunning_total
2024-01-151299.991299.99
2024-01-1699.981399.97
2024-01-17349.991749.96
2024-01-1879.991829.95
2024-01-19599.992429.94
2024-01-20159.992589.93

Each row shows its own total plus everything before it in the ordering. Every row survives — that is the window-function contract: GROUP BY would have collapsed these 40 orders into buckets; OVER annotates them in place.

Per-group running totals: add PARTITION BY

Each customer's spend-to-date — the numbering restarts per customer
SELECT customer_id, order_date, total,
  SUM(total) OVER (
    PARTITION BY customer_id      -- restart per customer
    ORDER BY order_date, order_id  -- accumulate in time order
  ) AS customer_running
FROM orders;

This is the shape of half the "running" questions in practice: running balance per account, cumulative points per player, spend-to-date per customer. PARTITION BY picks the lane, ORDER BY picks the direction, SUM accumulates.

The tie trap: why order_id is in the ORDER BY

The default frame is RANGE, and RANGE lumps ties together. With ORDER BY order_date alone, two orders on the same date get the same running total — both include both. Adding a unique tiebreaker (, order_id) makes each row's total include itself and everything strictly before it, which is almost always what a "running balance" means. Same fix, different symptom, as the ROW_NUMBER tiebreaker — ties are where window queries go to misbehave: see the ranking version.

The self-join it replaced (still in old code)

Pre-window-function spelling — correct, and O(n²)
SELECT a.order_date, a.total, SUM(b.total) AS running_total
FROM orders a
JOIN orders b ON b.order_id <= a.order_id
GROUP BY a.order_id, a.order_date, a.total;

Every row joins to all rows before it — 40 orders make 820 pairs; a million orders make half a trillion. You will meet this in legacy reports and old Stack Overflow answers. Recognize it, replace it with the OVER version, take the credit.

▶ Write it yourself: Running Total of Orders

FAQ

What is a running total in SQL?

A running total (cumulative sum) is a column where each row shows its own value plus the sum of every row before it in a given order — revenue to date, balance after each transaction. In SQL it is a window function: SUM(amount) OVER (ORDER BY date_col, id). No GROUP BY, every row stays in the result, and PARTITION BY restarts the total per group.

How do I do a running total that resets each month?

Partition by the month: PARTITION BY strftime('%Y-%m', order_date) ORDER BY order_date. Reset-per-X is always "X goes in PARTITION BY".

Running average? Running max?

Same skeleton, different aggregate: AVG(total) OVER (ORDER BY ...), MAX(total) OVER (ORDER BY ...). For moving (last-7-rows) versions, add a frame: ROWS BETWEEN 6 PRECEDING AND CURRENT ROW.

Percent of total to date?

Divide the running sum by the grand total: SUM(total) OVER (ORDER BY d) * 100.0 / SUM(total) OVER () — the empty OVER () is the whole-table sum. This is the Pareto/cumulative-share pattern interviews like.

One OVER clause from a column to a balance.

The window track goes running totals → rolling averages → frames, on real order data, with a Coach that shows what your window actually summed.

Start with one question: Running Total of Orders →

or browse the whole window-function set →