Hook: Interviewers love this one because a running total looks simple, but it quietly tests whether you really understand ordering, window functions, and edge cases.
Question: How do you calculate a running total in SQL?
Answer: A running total is a total that grows row by row as you move through ordered data. In SQL, the cleanest way to do it is with a window function: SUM(...) OVER (ORDER BY ...). If you need the total to restart for each customer, account, or category, you add PARTITION BY.
Interview-Ready Answer: I would use a window function with SUM(...) OVER and an ORDER BY clause, because that gives me a cumulative total without collapsing the rows. If I need separate totals per group, I add PARTITION BY. One detail I always watch is the frame and tie order: for a true row-by-row running total, I prefer ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW and a stable sort key.
Detailed Explanation: A running total is the sum of all values from the beginning of a sequence up to the current row. Think of it like a receipt tape: each new line adds to the total, but earlier lines stay visible. In SQL, this is usually done with a window function (a function that looks at a set of rows related to the current row, without grouping them away).
SUM adds the values in that frame. As the current row moves forward, the frame grows from the first row to the current row.PARTITION BY, the running total restarts at each partition, such as each account or customer.ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, the database computes a true row-by-row cumulative total, which is usually what interviewers want.| Approach | How it works | Pros | Cons |
|---|---|---|---|
| Window function | One pass over ordered rows | Clear, fast, keeps rows | Needs correct ordering |
| Correlated subquery | Sum all prior rows per row | Easy to read for small data | Slow on large tables |
| Self-join | Join each row to prior rows | Works in older SQL | Often much heavier |
Most databases do one sort plus one scan for the window calculation, so the cost is usually about O(n log n) because of sorting, with the cumulative step itself being close to O(n). On 100,000 rows this is usually fine; on millions of rows, the sort and memory use matter, so an index that matches the partition and order columns can help the planner read rows in order. A few important gotchas: if your order column has ties, rows may share the same value unless you add a tie-breaker; if values can be NULL, decide whether they should act like zero; and if the business meaning needs resets, use PARTITION BY rather than trying to fake it with a filter. Also remember that some systems default to a RANGE frame when you only say ORDER BY, which can group tied values together; ROWS is the safer choice for a strict running count of physical rows.
Real-World Story: Imagine a checkout service for an online store. The finance team wants a per-day cash balance for each merchant account, so every transaction row needs to show the balance after that payment, refund, or fee. A running total makes that report easy: order the ledger rows by timestamp, partition by account, and sum amounts as you go.
What goes wrong when people misunderstand it? A developer might group by day first and then try to compute the total, which removes the row-level detail. The dashboard then shows one number per day instead of a true cumulative line. In production, that looks like a line chart with sudden jumps, finance reconciliation mismatches, and logs where same-timestamp rows appear with the same total when they should not. Users notice that yesterday's balance does not match today's exported report, and support sees complaints like 'my refund is there, but the balance is off by 20 dollars.'
-- Running total per account, with a stable row order and a NULL-safe amount.
-- This is the common pattern interviewers want: SUM + OVER + ORDER BY.
-- We add PARTITION BY to restart the total for each account.
-- We add ROWS ... CURRENT ROW so duplicates in the date column do not blur rows together.
WITH transactions(account_id, txn_date, txn_id, amount) AS (
VALUES
(10, '2024-01-01', 1, 100.00),
(10, '2024-01-01', 2, NULL), -- edge case: missing amount should behave like 0
(10, '2024-01-02', 3, -20.00), -- refund reduces the running balance
(10, '2024-01-03', 4, 50.00),
(20, '2024-01-01', 5, 200.00),
(20, '2024-01-02', 6, 25.00)
)
SELECT
account_id,
txn_date,
txn_id,
amount,
SUM(COALESCE(amount, 0)) OVER (
PARTITION BY account_id
ORDER BY txn_date, txn_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_balance
FROM transactions
ORDER BY account_id, txn_date, txn_id;Follow-up & Tricky Questions:
PARTITION BY customer_id. That tells SQL to calculate a separate cumulative sum inside each customer group.ORDER BY so the sequence is stable and repeatable.COALESCE(amount, 0) or wrap the final expression with COALESCE if your business rule wants zero for empty sums.ROWS sometimes better than just ORDER BY? Because some databases use a default frame that behaves like RANGE, which can treat tied order values as one peer group. ROWS forces row-by-row accumulation.ORDER BY entirely? Then it is no longer a running total; you get the same total repeated for every row in the partition because there is no sequence to accumulate over.SUM include NULL values? No, SUM ignores NULL, but a frame containing only NULL values can still produce NULL, so make the business rule explicit with COALESCE when needed.Common Mistakes:
ORDER BY inside OVER.PARTITION BY when totals must restart per group. Fix: partition by customer, account, or category as needed.NULL always means zero. Fix: decide the rule explicitly with COALESCE.Memory Hook: Picture a receipt tape rolling across the table: each new line adds to the tape, but the old numbers stay visible. Running total = receipt tape sum.
Cheat Sheet:
SUM(amount) OVER (ORDER BY ...) is the core pattern.PARTITION BY restarts the total for each group.ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW gives a strict row-by-row total.ORDER BY for duplicate dates or timestamps.COALESCE if missing amounts should count as zero.Practice Tasks: