Which Readings Belong in This Rolling Total?
Choose ROWS, peer groups and time ranges deliberately so rolling calculations include the observations the question actually describes.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
An Ordered Group Still Needed a Boundary
BOTIE
That explains our apparent jump. If timestamp alone defines peers, both records at that timestamp can enter a cumulative frame together. A physical-row frame and a complete ordering express a different, equally deliberate calculation.
Sources for this history
The Total Jumps Before We Reach the Second Reading
Two usage readings arrive with the same timestamp. Botie expects a running total to increase after each record, but both records suddenly display the total after both readings. The database has followed its frame rule; we had not actually specified the row-by-row rule we intended.
A window frame names the rows allowed to contribute to the current answer. We will compare a running total with a two-reading total, then decide what a tied timestamp and a missing day should do.
The records are invented maintenance examples. The workshop exposes record operations; the SQL worksheet executes a real query in an isolated SQLite database.
Step 1 of 1
Move the two-row frame through P7
P7’s ordered amounts are 2, 3 and 7. The first two timestamps tie; the third is two days later.
| Current event | Rows in the frame | Total |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 1 and 2 | 5 |
| 3 | 2 and 3 | 10 |
1
- Rows in the frame
- 1
- Total
- 2
2
- Rows in the frame
- 1 and 2
- Total
- 5
3
- Rows in the frame
- 2 and 3
- Total
- 10
A frame moves while the original rows remain
The frame moves through sorted records and resets independently for each asset.
Build a two-observation running report
Order within each pump and sum the current reading with its immediately preceding reading.
BOTIE
One recent_total per usage record
[
{
"event_id": 1,
"asset_id": "P7",
"recent_total": 2
},
{
"event_id": 2,
"asset_id": "P7",
"recent_total": 5
},
{
"event_id": 3,
"asset_id": "P7",
"recent_total": 10
},
{
"event_id": 4,
"asset_id": "P8",
"recent_total": 10
}
]Follow the operations
1. Usage observations
[ { "event_id": 1, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 2 }, { "event_id": 2, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 3 }, { "event_id": 3, "asset_id": "P7", "measured_at": "2026-01-03 08:00:00", "amount": 7 }, { "event_id": 4, "asset_id": "P8", "measured_at": "2026-01-01 08:00:00", "amount": 10 } ]2. Order rows by named fields
[ { "event_id": 1, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 2 }, { "event_id": 2, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 3 }, { "event_id": 3, "asset_id": "P7", "measured_at": "2026-01-03 08:00:00", "amount": 7 }, { "event_id": 4, "asset_id": "P8", "measured_at": "2026-01-01 08:00:00", "amount": 10 } ]3. Number of preceding observations
1
4. Sum a trailing row frame
[ { "event_id": 1, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 2, "recent_total": 2 }, { "event_id": 2, "asset_id": "P7", "measured_at": "2026-01-01 08:00:00", "amount": 3, "recent_total": 5 }, { "event_id": 3, "asset_id": "P7", "measured_at": "2026-01-03 08:00:00", "amount": 7, "recent_total": 10 }, { "event_id": 4, "asset_id": "P8", "measured_at": "2026-01-01 08:00:00", "amount": 10, "recent_total": 10 } ]5. Choose table columns
[ { "event_id": 1, "asset_id": "P7", "recent_total": 2 }, { "event_id": 2, "asset_id": "P7", "recent_total": 5 }, { "event_id": 3, "asset_id": "P7", "recent_total": 10 }, { "event_id": 4, "asset_id": "P8", "recent_total": 10 } ]6. Result
[ { "event_id": 1, "asset_id": "P7", "recent_total": 2 }, { "event_id": 2, "asset_id": "P7", "recent_total": 5 }, { "event_id": 3, "asset_id": "P7", "recent_total": 10 }, { "event_id": 4, "asset_id": "P8", "recent_total": 10 } ]
This component implements a ROWS sum. It does not claim to implement RANGE, GROUPS or SQL query planning.
1. Separate the partition, order and frame
The partition selects the pump’s history. The ordering puts that history in sequence. The frame selects the portion of that ordered history used for the current aggregate.
These choices answer different questions. Correctly partitioning by asset does not make a two-day frame equivalent to a two-reading frame. Correctly ordering by time does not say whether all earlier records should contribute.
Say the calculation aloud before choosing its frame: current observation plus one preceding observation.
2. See the default peer behaviour directly
With an ordered aggregate and no explicit frame, SQLite uses a RANGE frame from the partition’s start through the current row’s peers. When timestamp alone orders the window, equal timestamps are peers.
In this example, the two 08:00 readings therefore both receive 5. This is useful when simultaneous readings should share a cumulative result. It is surprising if we expected event-by-event accumulation.
The outer event_id ordering displays the rows consistently without changing the window’s timestamp-only peer groups.
Compare peer-aware accumulation with explicit row accumulation
The two sums deliberately use different window orders and frames.
| event_id | asset_id | measured_at | amount |
|---|---|---|---|
| 1 | P7 | 2026-01-01 08:00:00 | 2 |
| 2 | P7 | 2026-01-01 08:00:00 | 3 |
| 3 | P7 | 2026-01-03 08:00:00 | 7 |
| 4 | P8 | 2026-01-01 08:00:00 | 10 |
Database setup for this example
CREATE TABLE usage(event_id INTEGER PRIMARY KEY, asset_id TEXT NOT NULL, measured_at TEXT NOT NULL, amount INTEGER);
INSERT INTO usage VALUES (1,'P7','2026-01-01 08:00:00',2),(2,'P7','2026-01-01 08:00:00',3),(3,'P7','2026-01-03 08:00:00',7),(4,'P8','2026-01-01 08:00:00',10);SELECT event_id,
SUM(amount) OVER (PARTITION BY asset_id ORDER BY measured_at) AS peer_total,
SUM(amount) OVER (PARTITION BY asset_id ORDER BY measured_at, event_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS row_total
FROM usage WHERE asset_id='P7' ORDER BY event_id;| event_id | peer_total | row_total |
|---|---|---|
| 1 | 5 | 2 |
| 2 | 5 | 5 |
| 3 | 12 | 12 |
Neither total is random. The first includes the tied peer immediately; the second follows the chosen event-ID order.
3. Write the two-reading boundary explicitly
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW includes the previous ordered record and the current one. The first row has no predecessor, so its frame contains only itself.
A second ordering column makes the sequence deterministic when timestamps tie. SUM ignores NULL amounts; if the entire frame is unknown, its result is NULL. We do not replace missing measurements with zero without a separate decision.
A two-row frame can contain fewer than two known values.
Compare a wider three-record frame
This worked query allows two preceding rows, while the challenge asks for only one. P8 still begins its own partition.
| event_id | asset_id | measured_at | amount |
|---|---|---|---|
| 1 | P7 | 2026-01-01 08:00:00 | 2 |
| 2 | P7 | 2026-01-01 08:00:00 | 3 |
| 3 | P7 | 2026-01-03 08:00:00 | 7 |
| 4 | P8 | 2026-01-01 08:00:00 | 10 |
Database setup for this example
CREATE TABLE usage(event_id INTEGER PRIMARY KEY, asset_id TEXT NOT NULL, measured_at TEXT NOT NULL, amount INTEGER);
INSERT INTO usage VALUES (1,'P7','2026-01-01 08:00:00',2),(2,'P7','2026-01-01 08:00:00',3),(3,'P7','2026-01-03 08:00:00',7),(4,'P8','2026-01-01 08:00:00',10);SELECT event_id, asset_id,
SUM(amount) OVER (
PARTITION BY asset_id ORDER BY measured_at, event_id
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS recent_total
FROM usage ORDER BY asset_id, measured_at, event_id;| event_id | asset_id | recent_total |
|---|---|---|
| 1 | P7 | 2 |
| 2 | P7 | 5 |
| 3 | P7 | 12 |
| 4 | P8 | 10 |
The third P7 frame includes all three amounts and totals 12. Reducing it to one preceding row would remove the first amount and leave 10.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
What is the third frame’s total?
Use the information below.
The ordered P7 amounts are 2, 3 and 7. The frame contains one preceding row and the current row.4. Do not turn a record count into a duration by renaming it
ROWS counts physical rows. GROUPS counts peer groups defined by the ordering values. RANGE defines a value-based boundary, with engine-specific rules for its ordering expression and offsets.
If the task asks for the preceding 24 hours, sparse measurements make a fixed row count unsuitable. Use an explicitly defined time-range calculation and verify its boundary and timestamp representation. In SQLite, a numeric timestamp expression can support suitable RANGE offsets; it should not be improvised by subtracting from arbitrary text.
The next date lesson makes missing calendar days and half-open time boundaries visible.
5. Check the frame before using LAST_VALUE
LAST_VALUE returns the last value inside its frame, which is not necessarily the last row of the entire partition. Under a cumulative default, the frame often ends at the current peer group.
For the last value over the complete ordered partition, specify ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING with a deterministic order. By contrast, LAG and LEAD use relative row positions and do not use the frame to limit their lookup in SQLite.
A function’s name does not override its documented frame behaviour.
6. Change the spacing and missingness independently
Move the third P7 observation a week later. A two-observation calculation should still combine it with the second observation. Set both amounts in one frame to NULL and predict an unknown total.
These tests separate what the frame counts from what SUM does with values inside it. When the output is wrong, inspect the ordered rows and frame membership before changing the arithmetic.
The workshop’s preceding-row count is adjustable, but it always counts observations rather than elapsed time.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
Does Friday’s frame include Monday?
Use the information below.
A pump has readings on Monday and Friday, with no readings between. The frame is ROWS BETWEEN 1 PRECEDING AND CURRENT ROW.Try it with Botie
Build with Botie
Write and run SQL