Running Totals: Intermediate
SUM OVER with the Right Frame
Recognize that any question asking for cumulative, year-to-date, or rolling aggregates is a running total pattern.
- ▸"compute the cumulative ... over time"
- ▸"running balance," "running total," "running count"
- ▸"year-to-date," "life-to-date," "month-to-date"
- ▸"7-day rolling ..." or "30-day trailing ..."
- ▸"What was the value as of date X?"
What the interviewer is actually testing
Pause before writing the query and ask one question: 'is this per account, or across all accounts?' That single clarifier signals that you treat the partition clause as a business decision, not a default. Most candidates assume per-account. Naming it out loud is the move of someone who has been burned by aggregating across the wrong grain.
The frame everyone should know by heart
- "I'll use SUM with a GROUP BY."
- Writes a per-date total, not a running total
- Has to be corrected by the interviewer
- Spends time recovering instead of going deep
- "This is a running total. I'll use SUM OVER, partitioned by account, ordered by date."
- States the frame clause explicitly: UNBOUNDED PRECEDING to CURRENT ROW
- Mentions ROWS vs RANGE before being asked
- Sets up the follow-up about the bug RANGE introduces
ROWS vs RANGE: The Silent Bug
Write a correct running total using SUM() OVER (ORDER BY ... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).
Why each clause matters
What happens without the frame clause
The Postgres docs explicitly state the default is RANGE. So do the SQL Server docs. Most candidates default to writing no frame at all because they have never read the docs. State the frame out loud while you type it: 'ROWS, not RANGE, so duplicate dates do not collapse.' That single sentence is what separates the answer that ships from the answer that ships a silent bug.
Output trace
| account_id | txn_date | amount | running_balance |
|---|---|---|---|
| A | 2025-01-01 | 100 | 100 |
| A | 2025-01-02 | 50 | 150 |
| A | 2025-01-05 | 200 | 350 |
| B | 2025-01-01 | 300 | 300 |
| B | 2025-01-03 | -100 | 200 |
The variant interviewers love: per-period running total with a reset
Rolling Windows (7-Day Trailing)
Explain why RANGE (the default) groups duplicate ORDER BY values and produces wrong running totals, while ROWS does not.
- ▸RANGE treats tied ORDER BY values as a peer group; ROWS treats them individually
- ▸Three transactions on the same day: RANGE returns 180, 180, 180; ROWS returns 100, 150, 180
- ▸Default is RANGE on every major engine
- ▸Fix is one word in the query: write ROWS explicitly
The bug, stated plainly
Concrete example of the bug
- Ties on the ORDER BY column share a single cumulative value
- Three transactions on the same day all get the same running total
- Looks reasonable in a quick spot-check
- Wrong when txn_date is the ORDER BY and multiple rows per day exist
- Every row gets its own cumulative value
- Ties on the ORDER BY column are broken by row position
- Matches what a business analyst means by 'running total'
- Write this explicitly; do not rely on the default
Why RANGE exists at all
The interviewer at Meta and Stripe specifically watches for this. They will write three rows with the same date in the test data, run your query, and watch you not notice. If you write ROWS unprompted, you get a point. If you do not, and the interviewer says 'walk me through your output,' you have to find the bug live. Finding it is recoverable. Missing it after they hint is not.
Other frame clauses worth knowing
| Situation | Phrasing that flatlines | Phrasing that lands |
|---|---|---|
| You see 'cumulative balance per account' | "SUM with a GROUP BY." | "This is a running total. SUM OVER (PARTITION BY account_id ORDER BY txn_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). One row per input row, not one per group." |
| The interviewer hands you ties on date | "It still works." | "With the default RANGE frame, ties on ORDER BY collapse into one peer-group value. ROWS gives each row its own cumulative value. I always write ROWS explicitly; the default is a silent bug." |
| The interviewer asks 'scale to a billion rows' | "Window functions are fast." | "Pre-aggregate to daily grain inside a CTE first, then run the window over the daily totals. The window cost is dominated by per-partition sort; pre-aggregation cuts the row count, and partition layout decides whether the sort is local or shuffled." |
| The interviewer asks 'how would you stream this' | "Maintain state per account." | "Streams hate running totals. I'd let a nightly batch compute history over the daily aggregate and have the stream handle current-day only; late arrivers reconcile next day. Pure-stream running totals require unbounded state per partition." |
| The interviewer asks for 'only positive transactions' | "WHERE amount > 0." | "WHERE filters rows out of the output, which changes the row count. SUM(CASE WHEN amount > 0 THEN amount END) keeps every row present and only contributes matching ones; SUM ignores NULL. 'Filter the input' versus 'filter the contribution' are different contracts." |
Resetting the Total with PARTITION BY
Extend running totals to 7-day rolling averages, monthly resets, and conditional cumulative sums.
7-day rolling sum
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW is seven rows, not six. The frame is inclusive on both sides. Counting off by one here is a classic interview mistake. Read it out loud: '6 preceding through current, that's seven rows.'
Monthly reset
Conditional running totals
- ▸7-day rolling: change the frame clause to ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
- ▸Monthly reset: change the partition to add DATE_TRUNC('month', txn_date)
- ▸Conditional total: change the source expression to SUM(CASE WHEN ... THEN amount END)
The pattern, restated
- Rewrites the entire query from scratch for each variant
- Has to recall the full syntax every time
- Treats the variants as separate problems
- Slow on follow-ups; runs out of time before the third probe
- Identifies which of the three pieces needs to change
- Changes that one piece and narrates why
- Treats the canonical form as a template, not a problem
- Handles four follow-ups in the time most candidates handle one
Same-Timestamp Rows and RANGE Surprises
Discuss partition-level parallelism, pre-aggregation tradeoffs, and why running totals are expensive on streaming systems.
Window function parallelism
Pre-aggregation when the grain is coarser than the source
Mention pre-aggregation unprompted. 'For scale, I'd pre-aggregate to daily grain inside a CTE, then run the running total on the daily totals.' This single sentence tells the interviewer you have built dashboards that ran nightly on real data and learned that you do not run window functions on raw event streams when the consumer wants daily numbers.
Why running totals are hard in streaming
When the window function is the wrong tool
Partitioned vs clustered table layouts
- Keep unbounded state per partition in the stream processor
- Memory grows linearly with active accounts
- Late-arriving events require restating downstream rows
- Pages on-call when an account has too much history
- Compute running totals in a nightly batch over daily aggregates
- Stream layer handles current-day, batch layer handles history
- Memory bounded by the daily window, not by account history
- Late-arrivers are reconciled the next day, not in real time
The closing summary for this question
> You are in a Databricks data engineering interview. The interviewer asks: 'Compute month-to-date revenue per region per product, with a 7-day rolling component, on a billion-row transactions table.'
SUM(amount) OVER (PARTITION BY account_id ORDER BY txn_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), and each clause carries weight: partition sets the reset, order sets the sequence, frame sets the window.RANGE, which gives every row tied on the ORDER BY column the same cumulative value: three same-day transactions of 100, 50, and 30 report 180, 180, 180 instead of 100, 150, 180.6 PRECEDING for a 7-row trailing sum), the partition for the reset boundary (add DATE_TRUNC('month', txn_date)), or the source expression for what counts (a CASE that returns NULL for rows to skip).ROWS BETWEEN 6 PRECEDING AND CURRENT ROW is seven rows because the frame is inclusive on both ends, and the off-by-one here is a routine interview miss.PARTITION BY is what lets the engine shard the work, so hot partitions are the real ceiling, not partition count.GROUP BY when the consumer only needs the latest cumulative value per account, and keep the window function for when they need it at every intermediate point in time.The cumulative sum that trips up candidates who forget frame clauses
- Category
- SQL
- Difficulty
- intermediate
- Duration
- 25 minutes
- Challenges
- 0 hands-on challenges
Topics covered: SUM OVER with the Right Frame, ROWS vs RANGE: The Silent Bug, Rolling Windows (7-Day Trailing), Resetting the Total with PARTITION BY, Same-Timestamp Rows and RANGE Surprises
Lesson Sections
- SUM OVER with the Right Frame (concepts: sqlWindowFrame)
Here is the most common shape this question takes. The interviewer hands you a transactions table and says 'compute the running balance per account over time.' Two minutes later they have a sense of where you sit on the depth axis. Not from your syntax, but from how cleanly you say what you are doing. The answer that lands names the pattern, names the frame clause, and writes the query in twelve lines. The answer that flatlines writes the same twelve lines but cannot explain why the frame clause
- ROWS vs RANGE: The Silent Bug (concepts: sqlWindowFrame)
The canonical running total query is the one you should be able to write while talking. PARTITION BY the grouping column, ORDER BY the time column, frame clause UNBOUNDED PRECEDING to CURRENT ROW. The frame clause is what makes it a running total rather than a partition-wide aggregate. Why each clause matters PARTITION BY account_id: the running total resets at every new account. Without it, the SUM runs across every transaction in the table, regardless of which account it belongs to. ORDER BY t
- Rolling Windows (7-Day Trailing) (concepts: sqlWindowFrame)
This is the section the interviewer goes deepest on. The frame clause is where they probe whether you understand the silent bug that ships running totals to production with wrong numbers. The bug does not throw an error. It does not warn. It produces visibly plausible numbers that are subtly wrong. Anyone who has been on call for a reporting pipeline has seen this bug. Anyone who has not is about to learn it the hard way. The bug, stated plainly If two rows share the same ORDER BY value, RANGE t
- Resetting the Total with PARTITION BY (concepts: sqlWindowFrame)
Once the running total works, the interviewer will pivot. 'Now do a 7-day rolling sum.' 'Now reset every month.' 'Now only count positive transactions.' These variants test whether you understand the structure or just memorized the canonical query. Each variant changes exactly one part: the frame clause, the partition, or the source expression. 7-day rolling sum Change the frame clause. UNBOUNDED PRECEDING becomes 6 PRECEDING. The window is now seven rows wide instead of growing without bound. T
- Same-Timestamp Rows and RANGE Surprises (concepts: sqlAggregateOver)
'How does this perform on a billion-row table?' is the standard scale follow-up. The right answer covers three things: how window functions parallelize, what pre-aggregation buys you, and why streaming systems struggle with running totals. Nailing all three flips the verdict from hire to strong hire on this question. Window function parallelism PARTITION BY is what makes window functions scale. Each partition is computed independently, so the engine can shard the workload across cores or executo