Build an exact mental model of ordered MongoDB indexes by tracing equality, range, sort, coverage, explain evidence, and write/storage cost on AtlasMart orders.

How B-Tree-Like Indexes Support Equality, Range, Sort, and Covered Queries

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.

Intermediate100–135 minutesB-tree-like equality/range/sort/coverage labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

Learning objectives

01

Model a conventional MongoDB index as an ordered B-tree-like key structure rather than a magic “fast query” switch.

02

Connect equality, range, and sort predicates to contiguous regions of an ordered index.

03

Read explain execution statistics before and after index creation and distinguish keys examined from documents examined.

04

Recognize a covered query by the absence of collection document fetches and understand how projection choices can destroy coverage.

05

Measure index storage and write-maintenance cost before deciding an index is worth keeping.

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:27062. Authentication and TLS are disabled only for this isolated lab. Feature Compatibility Version (FCV) is 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 index folklore

Index behavior depends on query shape, projection, sort, data distribution, planner choice, cache state, topology, and patch version. The labs therefore inspect winningPlan, totalKeysExamined, totalDocsExamined, index definitions/sizes, and target query results. Small fixtures prove semantics, not production latency. Timing snippets are comparative demonstrations only, not benchmarks.

1. AtlasMart problem: a paid-order timeline is correct but scans too much

AtlasMart repeatedly asks for recent paid orders for one tenant above a minimum amount. Without a suitable secondary index, MongoDB may examine collection documents to find the answer. A conventional MongoDB index stores ordered key values plus references back to documents. “B-tree-like” describes the ordered search structure at the level needed for query design; the lesson does not pretend application developers manage physical B-tree pages directly.

Concept What it means
Index key One ordered value or tuple derived from indexed document fields.
Equality predicate Narrows the scan to one value or a contiguous prefix of values.
Range predicate Selects an interval such as $gte/$lt; it can widen the number of keys scanned.
Sort support The index already stores keys in an order compatible with the requested sort, avoiding a blocking sort stage.
Covered query All matching and returned fields come from index keys, so collection documents need not be fetched.
Write amplification Every relevant insert/update/delete must also maintain each affected index structure.

2. Seed a deterministic workload and observe the no-index baseline

bash · isolated Chapter 10 Lesson 1 lab setup
docker rm -f atlasmart-mongo-ch10-l1 2>/dev/null || truedocker volume rm atlasmart-mongo-ch10-l1-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch10-l1 \  -p 127.0.0.1:27062:27017 \  -v atlasmart-mongo-ch10-l1-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27062/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))' 
javascript · seed AtlasMart orders
const c=db.orders_ch10_l1;c.drop();c.insertMany([ {_id:1,orderId:"A-1001",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-01T09:00:00Z"),totalCents:1200}, {_id:2,orderId:"A-1002",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-01T10:00:00Z"),totalCents:3200}, {_id:3,orderId:"A-1003",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-01T11:00:00Z"),totalCents:2500}, {_id:4,orderId:"A-1004",tenantId:"tenant-a",status:"cancelled",createdAt:ISODate("2026-08-01T12:00:00Z"),totalCents:9000}, {_id:5,orderId:"A-1005",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-02T09:00:00Z"),totalCents:1800}, {_id:6,orderId:"A-1006",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-02T10:00:00Z"),totalCents:7400}, {_id:7,orderId:"B-2001",tenantId:"tenant-b",status:"paid",createdAt:ISODate("2026-08-01T09:30:00Z"),totalCents:5000}, {_id:8,orderId:"B-2002",tenantId:"tenant-b",status:"paid",createdAt:ISODate("2026-08-01T10:30:00Z"),totalCents:1300}, {_id:9,orderId:"B-2003",tenantId:"tenant-b",status:"refunded",createdAt:ISODate("2026-08-01T11:30:00Z"),totalCents:5000}, {_id:10,orderId:"A-1007",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-03T08:00:00Z"),totalCents:2100}, {_id:11,orderId:"A-1008",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-03T09:00:00Z"),totalCents:2200}, {_id:12,orderId:"A-1009",tenantId:"tenant-a",status:"paid",createdAt:ISODate("2026-08-03T10:00:00Z"),totalCents:2300}]);printjson({count:c.countDocuments({}),indexes:c.getIndexes()});
javascript · run query and executionStats before secondary index
const c=db.orders_ch10_l1;const filter={tenantId:"tenant-a",status:"paid",totalCents:{$gte:2000}};const projection={_id:0,orderId:1,createdAt:1,totalCents:1};const sort={createdAt:-1};printjson(c.find(filter,projection).sort(sort).toArray());printjson(c.find(filter,projection).sort(sort).explain("executionStats"));
text · deterministic business result
A-1009 2026-08-03T10:00:00Z 2300A-1008 2026-08-03T09:00:00Z 2200A-1007 2026-08-03T08:00:00Z 2100A-1006 2026-08-02T10:00:00Z 7400A-1003 2026-08-01T11:00:00Z 2500A-1002 2026-08-01T10:00:00Z 3200

The exact winning-plan JSON is version-sensitive, but before the secondary index the important evidence is that MongoDB has no workload-specific ordered access path. On this tiny collection a collection scan is cheap; that does not make the shape production-safe at millions of documents.

3. Build one index whose key order matches the request

The index below sorts first by tenant and status (equality fields), then by descending creation time (the requested order), then stores amount and order identifier. Conceptually, nearby keys look like tuples such as (tenant-a, paid, 2026-08-03T10:00Z, 2300, A-1009). The index contains encoded BSON key values, not a separate copy of the whole document.

javascript · create and inspect the compound index
const c=db.orders_ch10_l1;print(c.createIndex( {tenantId:1,status:1,createdAt:-1,totalCents:1,orderId:1}, {name:"idx_tenant_status_created_total_order"}));printjson(c.getIndexes());printjson(c.stats().indexSizes);
Conceptual key ordering

Keys for the same tenant/status form one contiguous region. Within that region, createdAt:-1 orders newer timestamps before older timestamps. totalCents comes after the sort field, so a minimum-amount predicate can be evaluated from index keys but may not bound the scan as tightly as if it appeared earlier. That tradeoff becomes Lesson 2’s ESR versus ERS decision.

4. Re-run with hint to isolate the candidate index

javascript · force candidate index for controlled comparison
const c=db.orders_ch10_l1;const q=c.find( {tenantId:"tenant-a",status:"paid",totalCents:{$gte:2000}}, {_id:0,orderId:1,createdAt:1,totalCents:1}).sort({createdAt:-1}).hint("idx_tenant_status_created_total_order");printjson(q.toArray());printjson(c.find( {tenantId:"tenant-a",status:"paid",totalCents:{$gte:2000}}, {_id:0,orderId:1,createdAt:1,totalCents:1}).sort({createdAt:-1}).hint("idx_tenant_status_created_total_order").explain("executionStats"));
What explain proves

Compare totalKeysExamined, totalDocsExamined, the presence of an IXSCAN, and whether a FETCH stage exists. executionTimeMillis on 12 documents is not a meaningful benchmark. A correct index can even look slower in a tiny warm-cache test because fixed overhead dominates.

5. Covered query boundary: one extra projected field changes the plan

The first projection needs only fields represented in the index and excludes _id; MongoDB can potentially answer from the index alone. Ask for statusNote, which is not indexed, and a document fetch becomes necessary even if that field is absent in most documents.

javascript · break coverage by requesting a non-indexed field
const c=db.orders_ch10_l1;printjson(c.find( {tenantId:"tenant-a",status:"paid",totalCents:{$gte:2000}}, {_id:0,orderId:1,createdAt:1,totalCents:1,statusNote:1}).sort({createdAt:-1}).hint("idx_tenant_status_created_total_order").explain("executionStats"));
Coverage is a query-shape property

An index is not “covered” in the abstract. Coverage belongs to a specific filter + projection + sort combination. Returning the indexed array field in a multikey query has additional restrictions covered in Lesson 3.

6. Indexes consume storage and every write maintains them

Index size is visible through collection statistics, but physical bytes depend on WiredTiger compression, data distribution, and allocation state. Write cost can be compared experimentally using identical disposable collections. The following test deliberately uses wall-clock timing only to demonstrate directionality; it is not a latency benchmark and should not be used for SLO sizing.

javascript · compare disposable insert maintenance cost
const plain=db.orders_ch10_l1_plain;const indexed=db.orders_ch10_l1_indexed;plain.drop(); indexed.drop();indexed.createIndex({tenantId:1,status:1,createdAt:-1,totalCents:1,orderId:1},{name:"workload_idx"});function batch(n){ const a=[]; for(let i=0;i<n;i++) a.push({_id:i,orderId:`T-${i}`,tenantId:`t-${i%20}`,status:i%5?"paid":"cancelled",createdAt:new Date(1760000000000+i*1000),totalCents:500+(i%10000)}); return a;}const docs=batch(3000);let t=Date.now(); plain.insertMany(docs); const plainMs=Date.now()-t;t=Date.now(); indexed.insertMany(docs); const indexedMs=Date.now()-t;printjson({plainMs,indexedMs,plainIndexBytes:plain.totalIndexSize(),indexedIndexBytes:indexed.totalIndexSize()});
Do not optimize from one timing

Run controlled representative workloads, multiple trials, warm/cold-state variants, and tail-latency measurements. More indexes can reduce read latency while increasing insert/update latency, cache pressure, build time, backup size, and recovery/rebuild work.

7. Verification, cleanup, and production judgment

Verification checklist

  • Exactly 12 source orders exist before the index experiment.
  • The paid-order business result remains identical before and after index creation.
  • The candidate index definition and size are visible through getIndexes()/indexSizes.
  • Explain evidence distinguishes collection document reads from index-key reads.
  • Adding a non-indexed projected field destroys the covered-query condition.
  • The write-cost demonstration is treated as comparative evidence, not a universal performance number.

Production judgment. Add an index only when a recurring query shape or invariant justifies its storage and write-maintenance cost. Indexes improve access paths; they do not change write concern, replication durability, or read consistency. On replica sets, every index is maintained on replica members; on sharded collections, index requirements interact with shard-key routing and unique-index restrictions. Treat index builds as operational changes with disk/headroom, replication lag, rollback, and observation plans. Lesson 2 turns this one good shape into a systematic compound-key ordering decision.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch10-l1docker volume rm atlasmart-mongo-ch10-l1-data

Check your understanding

  1. Why can an ordered index support both filtering and sorting?
  2. What does totalDocsExamined measure?
  3. What plan property indicates a covered query?
  4. Why is a tiny executionTimeMillis comparison weak evidence?
  5. What costs increase when another secondary index is added?
Review the answers

1. Equality/range predicates can select contiguous index regions while the stored key order can already match the requested sort.

2. The number of collection-document examinations during execution; it is not the number of returned documents.

3. The plan can answer from index keys without a collection fetch; explain shows index access without an IXSCAN descending from/feeding a FETCH for the covered portion.

4. Twelve documents are dominated by fixed overhead, cache state, and planner behavior; it does not represent production latency distributions.

5. Storage, cache footprint, insert/update/delete maintenance, build/rebuild work, backup footprint, and potentially replication/recovery cost.

Authoritative references

  • MongoDB 8.3 release notes — Current 8.3 baseline and patch-sensitive behavior; re-check before reproduction.
  • Indexes overview — Index concepts, names, build considerations, and index-management overview.
  • Explain results — IXSCAN/FETCH/COLLSCAN evidence, covered-query plans, keys/documents examined, and execution statistics.
  • Measure index use — Using $indexStats and explain evidence instead of intuition to manage indexes.
  • mongosh release notes — mongosh version used for chapter commands.
  • Create indexes to support queries — Covered-query concept, workload-driven indexing, and the warning that excessive indexes hurt write-heavy workloads.
  • Compound indexes — Ordered compound keys, field order, prefixes, sort order, and field-count limit.

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.