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.

Advanced100–135 minutes$setWindowFields ranking/time-window labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

Learning objectives

01

Define partitions, ordering, document windows, and range/time windows precisely.

02

Compute running and moving metrics without collapsing the original documents as $group would.

03

Use rank, denseRank, and documentNumber while separating ranking semantics from final presentation order.

04

Recognize sort requirements for bounded and time-range windows and the single-ascending-sort-field rule for time ranges.

05

Inspect memory evidence—including MongoDB 8.3 window explain memory fields—before treating a window pipeline as production-safe.

Reproducible lab baseline

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.

Evidence, not screenshots

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

bash · isolated Chapter 09 Lesson 4 lab setup
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}))' 
javascript · seed daily store sales
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

javascript · partition by store and order by day
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());
text · expected west-store values
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.

javascript · rank each region by revenue
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

javascript · missing sort and invalid time-range sort
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); }
Why these errors matter

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.

javascript · explain the moving-window pipeline
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);
Small-lab limitation

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 $sort is present because $setWindowFields itself 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.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch09-l4docker volume rm atlasmart-mongo-ch09-l4-data

Check your understanding

  1. How does a window stage differ from $group in output grain?
  2. Why does a time-range window use one ascending date sort field?
  3. Does $setWindowFields guarantee final result ordering?
  4. Why might rank and denseRank differ?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.