Two orders arrive at the same timestamp. Your revenue report adds both amounts on the first displayed row, repeats that total on the second, and still finishes with the correct daily sum. A finance reconciliation passes, but the intraday chart tells the wrong story about when a sales target was crossed.
EcomToolkit treats a running total as a sequence with a definition, not merely a SUM with an OVER clause. This guide explains how tied sort values, window frames and filtering affect ecommerce cumulative reporting. All orders and amounts below are hypothetical, with one currency and an agreed revenue eligibility rule.
Table of Contents
- Define the row before defining the window
- Reproduce the tied-timestamp surprise
- Choose an explicit frame and stable ordering
- Separate rows from calendar periods
- Keep filters from changing the starting balance
- Build fixtures that reveal semantic mistakes
- The EcomToolkit view
Define the row before defining the window
A row might represent an order, a payment, a refund event or a day. Those are different analytical grains. Summing payment attempts as though they were orders produces an incorrect result before a window function is involved. Write down the unique row key and the rule that includes or excludes each amount.
For an order-level view, decide whether the amount means gross sales, captured payments or another defined measure. For an event ledger, refunds may appear as negative movements on their own dates. Avoid switching between those models in the middle of an analysis because their cumulative paths answer different questions.
The PostgreSQL window tutorial explains how window calculations retain individual rows while operating over related rows. This is useful for attaching context to an order without collapsing the dataset into daily aggregates. It also means that duplicate source rows remain present unless you resolve them first.
Before investigating the frame, validate uniqueness and eligibility. Compare row count with distinct event count and reconcile the final amount to the source. If there is a mismatch, examine join fanout and revenue duplication separately. A frame correction cannot repair duplicated input.
Reproduce the tied-timestamp surprise
Consider orders 101 and 102, worth 40 and 60 units, both recorded at 09:00. Order 103 contributes another 25 at 09:05. In PostgreSQL, SUM(amount) OVER (ORDER BY occurred_at) uses a default frame that includes peers sharing the current sort value. Both 09:00 rows therefore display 100.
That result is consistent with a cumulative total through the entire timestamp group. It is not a step-by-step order total. Neither interpretation is automatically wrong; the problem is presenting one while promising the other. Precision matters when a report assigns credit for crossing a threshold or triggers an operational alert.
| Order | Timestamp | Amount | Default timestamp frame | Ordered row frame |
|---|---|---|---|---|
| 101 | 09:00 | 40 | 100 | 40 |
| 102 | 09:00 | 60 | 100 | 100 |
| 103 | 09:05 | 25 | 125 | 125 |
The final value is 125 in both columns. A test that checks only that value misses the semantic difference. Check intermediate rows against expectations as well, especially where timestamps repeat because a source records only seconds or a batch import assigns the same ingestion time to many orders.

Choose an explicit frame and stable ordering
For a deterministic order-by-order accumulation, use a stable tie-breaker and an explicit row frame. The following example assumes order_id uniquely identifies a row and acts as an agreed display sequence for equal timestamps. It does not claim to recover a physical event order that the source never recorded.
SELECT order_id, occurred_at, amount,
SUM(amount) OVER (
ORDER BY occurred_at, order_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_amount
FROM eligible_orders
ORDER BY occurred_at, order_id;
The window ORDER BY controls calculation order; the outer ORDER BY controls presentation. Specify both when the report promises a sequence. Merely changing the outer sort can rearrange the displayed rows without changing the values already computed by the window expression.
The PostgreSQL window-function reference distinguishes frames from partitions and describes peer behaviour. A partition identifies the group, such as store and currency. A frame identifies the subset relevant to the current row. Treat these as two separate choices in review.
Partition by currency unless amounts have already been converted under an explicit policy. Similarly, partition by store when the dashboard is intended to show separate store totals. A globally ordered sum across unrelated currencies or merchants is mathematically computable but commercially meaningless.
Separate rows from calendar periods
A window containing six preceding rows plus the current row is seven rows, not necessarily seven days. If the source has one row per order, it measures seven orders. Even one row per trading day can skip a calendar date when there are no sales or an import is missing.
For a calendar-based daily chart, aggregate to one row per day in the chosen reporting timezone, then join to a complete date spine. Decide whether a missing day is a confirmed zero or unavailable data before filling it. Those states should not silently become the same number.
Use separate fields for reporting date and ingestion time. Late-arriving orders may legitimately restate an earlier event-date total, while an ingestion-date chart shows when the warehouse received them. Label the chosen clock and retain an as-of timestamp when reports are used to explain operational decisions.
If you need a true interval-based frame, check the syntax and behaviour in your specific database. SQL engines differ in support for RANGE intervals and ordering types. An explicit daily model is often easier for trading teams to inspect than a compact query whose date assumptions remain hidden.
Keep filters from changing the starting balance
Window functions operate on the rows available at their query stage. If a WHERE clause removes all events before Monday, a running sum in that query starts on Monday. That may be correct for weekly sales, but it is not a lifetime cumulative balance displayed from Monday onward.
To retain earlier history while showing a recent slice, calculate the cumulative result in a subquery or common table expression and apply the display filter outside it. Alternatively, compute a verified opening balance and add recent movements. Choose the approach that meets both correctness and warehouse cost requirements.
| Report requirement | Input or frame decision | Failure to test |
|---|---|---|
| Order-by-order daily progress | Stable order with row frame | Equal timestamps |
| Total through a timestamp group | Preserve peer grouping intentionally | Partial peer display |
| Seven calendar days | Daily grain and complete dates | Zero-sales and missing days |
| Lifetime balance shown this week | Calculate history before display filter | Lost opening balance |
| Separate market totals | Explicit market and currency partitions | Cross-market mixing |
Document whether refunds restate their original order or enter as new negative events. A refund posted today may reduce a cash-movement curve today while changing historical net-sales reporting under a different policy. The query should implement the chosen measure, not decide it implicitly.

Build fixtures that reveal semantic mistakes
A useful fixture contains tied timestamps, a negative movement, a missing calendar day, two currencies and a display filter that begins after the first event. Keep expected values small enough to calculate by hand. Ask another reviewer to derive the expected sequence from the written business rule before reading the SQL.
Test total reconciliation and intermediate state. For the three-order example, verify 40, 100 and 125 for the ordered row interpretation, and 100, 100 and 125 for the timestamp-group interpretation. This establishes that the query implements a selected meaning instead of merely producing plausible numbers.
Add a repeatability check after reloading the same records in a different physical order. A stable business key should preserve the displayed sequence. If it does not, identify the missing tie-breaker. Avoid depending on a database’s incidental storage order or a chart library’s default sorting behaviour.
Review dashboards that consume the result. Summing a running-total column again produces nonsense, and averaging cumulative balances answers a different question from summing daily revenue. Name the column clearly, document its permitted aggregation, and prevent an automatic chart builder from treating it as an ordinary additive measure.
Related SQL NULL reporting checks help distinguish absent amounts from real zeros. Keep that policy visible in the same model documentation so a later cleanup does not erase the assumptions that made the sequence correct.
The EcomToolkit view
A correct final sum is insufficient evidence for a correct revenue timeline. The grain, order, frame, partition and display filter all carry business meaning. Make them explicit, then verify the intermediate values that trading teams use to decide what happened and when.
Use a focused EcomToolkit analytics review to examine one disputed chart alongside its source fixture. A small, reproducible sequence usually resolves the question faster than debating screenshots of a large dashboard.
For a related analytical step, read our guide to building a complete sales calendar without turning unknown days into zeros.