Compute AtlasMart running totals, moving time windows, and rankings with correct partition/order semantics, explicit final sorting, and MongoDB 8.3 memory evidence.
$setWindowFields, Ranking, Moving Windows, Time-Based Analytics, and Partitioning
Batch heterogeneous writes safely, interpret partial success, compare ordered and unordered execution, and use modern cross-namespace bulk APIs without assuming all-or-nothing behavior.
Learning objectives
Define partitions, ordering, document windows, and range/time windows precisely.
Compute running and moving metrics without collapsing the original documents as $group would.
Use rank, denseRank, and documentNumber while separating ranking semantics from final presentation order.
Recognize sort requirements for bounded and time-range windows and the single-ascending-sort-field rule for time ranges.
Inspect memory evidence—including MongoDB 8.3 window explain memory fields—before treating a window pipeline as production-safe.
This lesson pins MongoDB Community Server
8.3.8 with
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
and mongosh 2.10.0. The server is a disposable
standalone published only on loopback
127.0.0.1:27060. Authentication and TLS are
disabled only for this isolated lab. Feature Compatibility
Version (FCV) and allowDiskUseByDefault are
observed but never changed. Default read/write concern and
primary read preference apply. Atlas, Search, KMS, Enterprise
Advanced, and paid services are not required. Runtime output
shown as “expected” is documentation-derived because this
generation environment has no Docker/mongod/mongosh runtime.
The exact optimizer tree, execution counters, spill fields, and stage-specific explain shape can vary with patch version, FCV, indexes, data distribution, and topology. The lesson therefore names the invariant to verify—matched documents, traversal set/depth, facet counts, window values, indexes used, disk-use evidence, and target collection state—instead of requiring byte-for-byte explain output.
1. Window functions preserve row grain
$setWindowFields divides input documents into
partitions, orders documents within each partition when
required, and appends calculated fields based on a window around
each current document. Unlike $group, it normally
preserves one output document per input document. This is ideal
for running totals, moving windows, ranks, and time-based
comparisons.
| Concept | Meaning |
|---|---|
| partitionBy | Expression defining independent groups; omitted means one partition for the full input. |
| sortBy | Ordering used by operators/windows that require position or range. |
| documents window | Boundaries expressed as row positions such as the previous two documents through current. |
| range window | Boundaries expressed as values around the current sort key. |
| time range window | A range window with a date sort key and a unit such as day/hour. |
| $rank | $rank gives tied values the same rank and can leave gaps; $denseRank removes gaps; $documentNumber numbers document positions. |
| output order |
The stage itself does not guarantee returned document
order; add a final $sort when the consumer
requires one.
|
2. Seed two store partitions
docker rm -f atlasmart-mongo-ch09-l4 2>/dev/null || truedocker volume rm atlasmart-mongo-ch09-l4-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch09-l4 \ -p 127.0.0.1:27060:27017 \ -v atlasmart-mongo-ch09-l4-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27060/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1,allowDiskUseByDefault:1}))'
const c=db.daily_store_sales_ch09_l4;c.drop();c.insertMany([ {_id:"w1-d1",storeId:"west",region:"west",day:ISODate("2026-08-01T00:00:00Z"),revenueCents:1000}, {_id:"w1-d2",storeId:"west",region:"west",day:ISODate("2026-08-02T00:00:00Z"),revenueCents:1800}, {_id:"w1-d3",storeId:"west",region:"west",day:ISODate("2026-08-03T00:00:00Z"),revenueCents:1400}, {_id:"w1-d4",storeId:"west",region:"west",day:ISODate("2026-08-04T00:00:00Z"),revenueCents:2200}, {_id:"e1-d1",storeId:"east",region:"east",day:ISODate("2026-08-01T00:00:00Z"),revenueCents:900}, {_id:"e1-d2",storeId:"east",region:"east",day:ISODate("2026-08-02T00:00:00Z"),revenueCents:1600}, {_id:"e1-d3",storeId:"east",region:"east",day:ISODate("2026-08-03T00:00:00Z"),revenueCents:2500}, {_id:"e1-d4",storeId:"east",region:"east",day:ISODate("2026-08-04T00:00:00Z"),revenueCents:1300}]);c.createIndex({storeId:1,day:1});printjson({count:c.countDocuments({}),indexes:c.getIndexes()});
3. Running total and trailing three-day time window
const p=[ {$setWindowFields:{ partitionBy:"$storeId", sortBy:{day:1}, output:{ runningRevenueCents:{$sum:"$revenueCents",window:{documents:["unbounded","current"]}}, trailing3DayRevenueCents:{$sum:"$revenueCents",window:{range:[-2,0],unit:"day"}} } }}, {$sort:{storeId:1,day:1}}, {$project:{_id:0,storeId:1,day:1,revenueCents:1,runningRevenueCents:1,trailing3DayRevenueCents:1}}];printjson(c.aggregate(p,{allowDiskUse:true}).toArray());
2026-08-01 revenue=1000 running=1000 trailing3day=10002026-08-02 revenue=1800 running=2800 trailing3day=28002026-08-03 revenue=1400 running=4200 trailing3day=42002026-08-04 revenue=2200 running=6400 trailing3day=5400 // Aug 2-4
The running total uses a document window from
unbounded to current. The trailing
metric uses a time range of two days before the current day
through the current day, so it is based on date values, not “the
previous two rows” if dates are missing.
4. Ranking is a different order contract
Revenue ranking needs revenue-descending order, not day order,
so use a separate window stage/pipeline. Rank operators use an
implicit window and must not be given an explicit
window option.
printjson(c.aggregate([ {$setWindowFields:{partitionBy:"$region",sortBy:{revenueCents:-1},output:{ rank:{$rank:{}},denseRank:{$denseRank:{}},rowNumber:{$documentNumber:{}} }}}, {$sort:{region:1,rank:1,_id:1}}, {$project:{_id:1,region:1,revenueCents:1,rank:1,denseRank:1,rowNumber:1}}]).toArray());
Our fixture has unique revenue values, so rank and denseRank
happen to be equal. Add ties in a test fixture if tie behavior
is business-critical. rowNumber is document
position in the defined order, not a stable identifier.
5. Controlled failures expose sort requirements
try { printjson(c.aggregate([{$setWindowFields:{partitionBy:"$storeId",output:{ trailing:{$sum:"$revenueCents",window:{documents:[-2,0]}} }}}]).toArray());} catch(e) { print("expected missing-sort failure:",e.codeName||e.name,e.message); }try { printjson(c.aggregate([{$setWindowFields:{partitionBy:"$storeId",sortBy:{day:1,_id:1},output:{ trailing:{$sum:"$revenueCents",window:{range:[-2,0],unit:"day"}} }}}]).toArray());} catch(e) { print("expected time-range sort restriction:",e.codeName||e.name,e.message); }
A bounded documents window requires sortBy; a
time-range window must sort on one date field in ascending
order. If deterministic presentation also needs a tie-breaker,
add a separate final $sort rather than violating
the time-range rule.
6. Memory and explain in MongoDB 8.3
$setWindowFields can retain substantial state for
large partitions and is among stages governed by aggregation
memory/disk-use rules. MongoDB 8.3 adds
peakTrackedMemBytes to explain output for window
execution, giving a direct signal for maximum tracked memory.
Treat that as workload evidence, not a universal threshold.
const e=c.explain("executionStats").aggregate([ {$setWindowFields:{partitionBy:"$storeId",sortBy:{day:1},output:{ trailing3DayRevenueCents:{$sum:"$revenueCents",window:{range:[-2,0],unit:"day"}} }}}, {$sort:{storeId:1,day:1}}],{allowDiskUse:true});printjson(e);
Eight rows cannot reproduce production spill or memory
pressure. Use production-like partition-size distributions and
skew, then inspect peakTrackedMemBytes,
usedDisk where applicable, temporary I/O, and
latency tails.
7. Verification, cleanup, and production judgment
Verification checklist
- Eight rows exist, four in each store partition.
- West running and trailing values match the listed arithmetic.
-
The final
$sortis present because$setWindowFieldsitself does not guarantee returned order. - Ranking is computed in a separate revenue-descending order contract.
- Both deliberate invalid-window examples raise errors.
- Explain is inspected for memory evidence; no spill is fabricated from the tiny fixture.
Production judgment. Window functions are
appropriate when analytic context is needed without collapsing
row grain. Cost is dominated by partition cardinality, ordering,
window width, indexes, memory, spill I/O, and skew. Avoid one
giant unpartitioned window for a naturally partitionable
workload. Tenant identity often belongs in
partitionBy or an earlier filter. On sharded
systems, measure where sorting/window work occurs and the
network/merge cost. Changes are read-only in this lesson, so
rollback is an application pipeline deployment rollback; any
cached/materialized derived results must be rebuilt separately.
Lesson 5 makes that last point concrete by writing derived
results back to collections with $merge and
$out.
docker rm -f atlasmart-mongo-ch09-l4docker volume rm atlasmart-mongo-ch09-l4-data
Check your understanding
- How does a window stage differ from $group in output grain?
- Why does a time-range window use one ascending date sort field?
- Does $setWindowFields guarantee final result ordering?
- Why might rank and denseRank differ?
- What new 8.3 explain signal is useful for window-memory analysis?
Review the answers
It appends calculations to each input document instead of collapsing many documents into one group result.
That is the documented contract for time-range windows; boundaries are interpreted as offsets from the date sort value.
No. Add an explicit final $sort if consumers require deterministic returned order.
When values tie, $rank can leave gaps after the tie whereas $denseRank does not.
peakTrackedMemBytes, which reports maximum tracked memory for the window execution in current 8.3 explain output.
Authoritative references
- MongoDB 8.3 release notes — Current 8.3 behavior and version-sensitive aggregation changes; re-check before reproducing.
- MongoDB aggregation pipeline — Ordered-stage execution model used throughout the chapter.
- Aggregation pipeline limits — Memory, disk-spill, stage-count, and 16 MiB output-document constraints.
- mongosh release notes — mongosh version used for the chapter commands.
- $setWindowFields stage — Partitions, sort/window restrictions, output-order non-guarantee, rank operators, and current 8.3 explain memory evidence.
- $rank expression — Ranking semantics and tie behavior.
- $denseRank expression — Dense ranking semantics.
- $documentNumber expression — Sequential document numbering within a window order.