Prompt 19 · Lesson 03 · Query/index/window evidence

Time-Series Indexes, Sort/Match Patterns, Window Analytics, and Bucket-Aware Performance

Use indexes and window pipelines from real query shapes, then verify their cost with explain evidence.

Advanced130–200 minutesIndex/window analytics labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Inspect the automatic meta/time index and add subfield-oriented indexes for real AtlasMart query shapes.

02

Use explain evidence to distinguish selective meta/time access from broad measurement scans without assuming a fixed internal plan format.

03

Build a $setWindowFields pipeline for moving time windows and enforce deterministic final ordering explicitly.

04

Explain how bucket indexing differs from ordinary per-document indexing and why time-series index restrictions matter.

05

Diagnose inefficient sort/match/window shapes and repair them with earlier selective predicates and appropriate indexes.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0 where a driver is used. The mandatory lab is a disposable standalone on loopback port 27162 named atlasmart-ch19-l3; replication or sharding is not required to learn the time-series mechanisms in this chapter. Authentication and TLS are disabled only for the isolated local lab. Feature Compatibility Version (FCV) is inspected. FCV is observed and never changed. Default read/write concern and primary read preference apply. Atlas, Search, Vector Search, KMS, and Enterprise Advanced are not mandatory. Internal system.buckets.* data and time-series diagnostic counters are used only for observation; they are not application APIs. Product commands were not executed in this generation environment because Docker, mongod, mongosh, and PyMongo are unavailable here, so environment-dependent bucket counts, storage ratios, explain plans, and throughput/latency values must be measured on the learner machine rather than copied as invented output. The lesson uses a medium deterministic fixture so explain and window behavior are observable, but it does not claim any universal latency or key-examination threshold.

1. Index the query shape, not the word “time-series”

AtlasMart's operations dashboard asks: “for tenant A, sensor 07, show the last six hours and a rolling 20-minute average.” The useful predicate contains stable metadata equality plus an event-time range, then requires chronological order. MongoDB 6.3+ creates a compound metaField/timeField index for new time-series collections, but that default index indexes the entire metadata subdocument. Queries on scalar metadata subfields often benefit from an explicit compound index matching those subfields and time.

Time-series secondary indexes are implemented over buckets, not one B-tree key per public measurement. MongoDB can index bucket min/max information and metadata to prune work efficiently, but several index families and operations have special restrictions. Always verify with explain() on the actual query shape.

2. Create a queryable fixture and inspect index definitions

start the disposable MongoDB 8.3.8 lab
docker rm -f atlasmart-ch19-l3 2>/dev/null || truedocker volume rm atlasmart-ch19-l3-data 2>/dev/null || truedocker run -d --name atlasmart-ch19-l3 \  -p 127.0.0.1:27162:27017 \  -v atlasmart-ch19-l3-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim \  --bind_ip_alldocker exec atlasmart-ch19-l3 mongosh --quiet --eval 'printjson(db.adminCommand({buildInfo:1}).version);printjson(db.adminCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion);' 
create collection, index, and 1,440 deterministic readings
const d=db.getSiblingDB("atlasmart");d.telemetry_ch19_l3.drop();d.createCollection("telemetry_ch19_l3",{timeseries:{timeField:"eventAt",metaField:"meta",granularity:"seconds"}});d.telemetry_ch19_l3.createIndex({"meta.tenantId":1,"meta.sensorId":1,eventAt:1},{name:"tenant_sensor_time"});const base=ISODate("2026-09-02T00:00:00Z");const docs=[];for (let sensor=0;sensor<4;sensor++) {  for (let i=0;i<360;i++) {    docs.push({eventAt:new Date(base.getTime()+i*2*60*1000),meta:{tenantId:"tenant-a",storeId:"baku-01",sensorId:`sensor-${sensor.toString().padStart(2,"0")}`},temperatureC:16+sensor+(i%20)/10,powerW:70+(i%40)});  }}d.telemetry_ch19_l3.insertMany(docs);print("measurements",d.telemetry_ch19_l3.countDocuments({}));printjson(d.telemetry_ch19_l3.getIndexes());

The fixture count is deterministically 1,440. Inspect both the automatically created time-series index and the explicit tenant_sensor_time index. Do not assume an _id_ index exists on a time-series collection: unlike regular collections it is not created automatically, and MongoDB 8.3 rejects creating or hinting an index named exactly _id_ on a time-series collection.

3. Compare selective and broad access with executionStats

explain two query shapes without hard-coding plan JSON
const d=db.getSiblingDB("atlasmart");const from=ISODate("2026-09-02T06:00:00Z"), to=ISODate("2026-09-02T10:00:00Z");const targeted=d.telemetry_ch19_l3.find({"meta.tenantId":"tenant-a","meta.sensorId":"sensor-01",eventAt:{$gte:from,$lt:to}}).sort({eventAt:1}).explain("executionStats");const broad=d.telemetry_ch19_l3.find({temperatureC:{$gte:17.5}}).sort({eventAt:1}).explain("executionStats");function stages(x,out=[]){if(!x||typeof x!=="object")return out;if(x.stage)out.push(x.stage);for(const v of Object.values(x))stages(v,out);return [...new Set(out)];}for (const [name,e] of [["targeted",targeted],["broad",broad]]) {  printjson({name,nReturned:e.executionStats?.nReturned,totalKeysExamined:e.executionStats?.totalKeysExamined,totalDocsExamined:e.executionStats?.totalDocsExamined,stages:stages(e.queryPlanner?.winningPlan)});}

The exact plan tree is version- and optimizer-dependent. The evidence to compare is returned rows, keys/documents examined, presence of index-oriented versus blocking-sort stages, and whether the intended predicate is pushed into bucket pruning. A correct result with broad bucket examination can still be operationally expensive.

4. Window analytics require partition, time order, and an explicit final sort

$setWindowFields lets AtlasMart compute moving statistics without exporting the series. A time-range window requires one ascending date sort field. The stage defines the order used for the window, but MongoDB does not guarantee the final returned order, so add a final $sort when consumers depend on output order.

rolling 20-minute temperature average
const d=db.getSiblingDB("atlasmart");const from=ISODate("2026-09-02T06:00:00Z"), to=ISODate("2026-09-02T08:00:00Z");const pipeline=[  {$match:{"meta.tenantId":"tenant-a","meta.sensorId":"sensor-01",eventAt:{$gte:from,$lt:to}}},  {$setWindowFields:{    partitionBy:"$meta.sensorId",    sortBy:{eventAt:1},    output:{      avgTemp20m:{$avg:"$temperatureC",window:{range:[-20,"current"],unit:"minute"}},      maxPower20m:{$max:"$powerW",window:{range:[-20,"current"],unit:"minute"}}    }  }},  {$project:{_id:0,eventAt:1,"meta.sensorId":1,temperatureC:1,powerW:1,avgTemp20m:1,maxPower20m:1}},  {$sort:{eventAt:1}}];printjson(d.telemetry_ch19_l3.aggregate(pipeline,{allowDiskUse:true}).limit(8).toArray());printjson(d.telemetry_ch19_l3.explain("executionStats").aggregate(pipeline));

Do not invent a specific spill result. This dataset is intentionally small and may remain in memory. In representative environments inspect explain/profiler/log memory and disk-use evidence. allowDiskUse:true permits eligible stages to spill; it is not proof that spilling occurred or that the pipeline is efficient.

5. Deliberately wrong: compute first, filter later

If a pipeline first derives a value for every measurement and only later filters on that computed value, the original metadata/time index cannot directly satisfy that computed predicate. A semantics-preserving optimizer may move ordinary matches earlier, so the bad example intentionally matches on a value created by $set.

bad computed predicate versus repaired stored-field predicate
const d=db.getSiblingDB("atlasmart");const from=ISODate("2026-09-02T06:00:00Z"), to=ISODate("2026-09-02T10:00:00Z");const bad=[  {$set:{wanted:{$and:[{$eq:["$meta.sensorId","sensor-01"]},{$gte:["$eventAt",from]},{$lt:["$eventAt",to]}]}}},  {$match:{wanted:true}},  {$sort:{eventAt:1}}];const good=[  {$match:{"meta.sensorId":"sensor-01",eventAt:{$gte:from,$lt:to}}},  {$sort:{eventAt:1}}];print("same logical count",d.telemetry_ch19_l3.aggregate(bad).itcount()===d.telemetry_ch19_l3.aggregate(good).itcount());printjson({bad:d.telemetry_ch19_l3.explain("executionStats").aggregate(bad),good:d.telemetry_ch19_l3.explain("executionStats").aggregate(good)});

The repair expresses selective conditions directly on stored fields before the expensive work. Compare explain evidence rather than assuming stage text alone determines execution. This is the same evidence-first habit developed in Chapters 8 and 12.

6. Production judgment

Indexes improve time-series queries when they align with stable metadata filters, time ranges, and required sort order. Every extra index adds storage and write maintenance, so use workload evidence before proliferating indexes. Watch index size, keys/docs examined, sort/window memory, bucket pruning, and tail latency across representative windows. Avoid distinct() on time-series collections for large distinct-value work; an indexed $match plus $group is the documented alternative.

Bridge. Lesson 4 adds expiration and shows why the collection's timeField determines what “old” means—an especially important distinction for late-arriving data.

cleanup only the chapter-specific lab
docker rm -f atlasmart-ch19-l3 2>/dev/null || truedocker volume rm atlasmart-ch19-l3-data 2>/dev/null || true

Check your understanding

  1. Why might the automatic meta/time index be insufficient for a query on meta.sensorId?
  2. Does $setWindowFields guarantee final result order?
  3. Why does the lesson inspect executionStats instead of looking only for IXSCAN?
  4. Does allowDiskUse:true prove a window stage spilled?
  5. Why is a computed wanted predicate intentionally a bad example?
Review the answers

1. The automatic index covers the whole metaField; explicit indexes on scalar metadata subfields can better match non-equality/subfield query shapes.

2. No. Its sortBy defines window order; add a final $sort when output order is part of the consumer contract.

3. Keys/docs examined and returned rows show actual work; plan labels alone do not quantify cost and may change across versions.

4. No. It only allows eligible disk use. Observe actual spill/memory evidence.

5. It prevents direct use of the stored-field index for that predicate and illustrates how query shape affects index eligibility.

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.