Design compound indexes from equality, sort, range, prefixes, and measured selectivity; compare ESR and ERS rather than treating field order as folklore.

Single-Field and Compound Indexes: ESR-Style Ordering and Prefix Rules

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.

Intermediate105–140 minutesESR/ERS + prefix explain labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

Learning objectives

01

Apply Equality–Sort–Range (ESR) as a workload guideline rather than a slogan.

02

Explain why leading compound-index prefixes are efficient access paths and why skipped leading fields weaken selectivity.

03

Compare ESR and Equality–Range–Sort (ERS) when a highly selective range may be worth an in-memory sort.

04

Reason about compound sort direction, including complete reversal of sort direction.

05

Use hint only as a controlled experiment and let representative explain evidence drive final index choice.

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:27063. 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. One query can justify different compound orders

AtlasMart needs “paid tenant-a orders above 8,000 cents, newest first.” The Equality–Sort–Range (ESR) guideline places exact-match fields first, then fields that must preserve sort order, then the range. But ESR is not a universal optimizer law. If a range is extremely selective, Equality–Range–Sort (ERS) may scan far fewer keys and accept a blocking sort. The correct choice depends on actual cardinality, sort cost, result limit, and latency objective.

Candidate Strength Cost/risk
{tenantId,status,createdAt,totalCents} Equality prefix + sort order preserved Amount range is after sort and may scan extra keys.
{tenantId,status,totalCents,createdAt} Equality + selective amount range Range before sort can prevent full index-provided sort and require a sort stage.
{totalCents,tenantId,status,createdAt} Range first may narrow by amount Throws away the selective tenant/status equality prefix and may scan unrelated tenants/statuses.

2. Seed a larger deterministic fixture and build three candidates

bash · isolated Chapter 10 Lesson 2 lab setup
docker rm -f atlasmart-mongo-ch10-l2 2>/dev/null || truedocker volume rm atlasmart-mongo-ch10-l2-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch10-l2 \  -p 127.0.0.1:27063:27017 \  -v atlasmart-mongo-ch10-l2-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27063/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))' 
javascript · seed 120 deterministic orders
const c=db.orders_ch10_l2;c.drop();const docs=[];for(let i=0;i<120;i++) docs.push({ _id:i+1, tenantId:i<90?"tenant-a":"tenant-b", status:i%6===0?"cancelled":"paid", createdAt:new Date(Date.UTC(2026,7,1)+i*3600000), totalCents:500+(i*137)%12000, orderId:`O-${String(i+1).padStart(4,"0")}`});c.insertMany(docs);printjson({count:c.countDocuments({}),paidA:c.countDocuments({tenantId:"tenant-a",status:"paid"})});
javascript · build ESR, range-first, and ERS candidates
const c=db.orders_ch10_l2;c.createIndex({tenantId:1,status:1,createdAt:-1,totalCents:1},{name:"idx_esr"});c.createIndex({totalCents:1,tenantId:1,status:1,createdAt:-1},{name:"idx_range_first"});c.createIndex({tenantId:1,status:1,totalCents:1,createdAt:-1},{name:"idx_ers"});printjson(c.getIndexes());

3. Compare identical query semantics with forced candidates

hint() is used here only to isolate each candidate. It is not a recommendation to hard-code hints in application code. Compare keys examined, documents examined, explicit sort stages, and returned rows.

javascript · executionStats for identical business query
const c=db.orders_ch10_l2;const filter={tenantId:"tenant-a",status:"paid",totalCents:{$gte:8000}};const sort={createdAt:-1};for(const name of ["idx_esr","idx_range_first","idx_ers"]){ print(`--- ${name} ---`); printjson(c.find(filter,{_id:0,orderId:1,createdAt:1,totalCents:1}).sort(sort).hint(name).explain("executionStats"));}
Interpretation

The best candidate is the one that meets the workload objective on representative data. ESR usually avoids the sort; ERS can win when the range is sufficiently selective. The range-first index often pays for ignoring the equality prefix. Exact counts depend on the generated values and planner implementation, so inspect rather than memorize.

4. Prefix rules: “can use” is not the same as “uses every field efficiently”

For {tenantId, status, createdAt, totalCents}, the leading prefixes are {tenantId}, {tenantId,status}, and {tenantId,status,createdAt}. MongoDB can efficiently seek on leading prefixes. A query on status alone has no leading tenantId bound. A query that constrains tenant but skips status can still use the tenant prefix, but the later createdAt condition cannot be used as if status had also been bound.

javascript · force idx_esr to expose prefix behavior
const c=db.orders_ch10_l2;const tests=[ ["prefix tenant",{tenantId:"tenant-a"}], ["prefix tenant+status",{tenantId:"tenant-a",status:"paid"}], ["skip leading field",{status:"paid"}], ["prefix plus skipped middle",{tenantId:"tenant-a",createdAt:{$gte:ISODate("2026-08-03T00:00:00Z")}}]];for(const [label,filter] of tests){ print(`--- ${label} ---`); printjson(c.find(filter,{_id:0,orderId:1}).hint("idx_esr").explain("executionStats"));}
Planner freedom

Without hint(), MongoDB may choose a collection scan or another index if scanning a weak prefix would cost more. “A compound index can support this query” is not a guarantee that the planner should select it.

5. Sort direction: same pattern or full reversal

An index with createdAt:-1 can traverse that sort key forward for descending order or backward for ascending order when the rest of the compound constraints are compatible. Mixed-direction multi-field sorts require the index directions to match the requested pattern or its full inverse; a partially inverted compound sort is different.

javascript · compare descending and reversed ascending traversal
const c=db.orders_ch10_l2;print("same direction");printjson(c.find({tenantId:"tenant-a",status:"paid"},{_id:0,createdAt:1,totalCents:1}).sort({createdAt:-1}).hint("idx_esr").explain("executionStats"));print("reverse direction");printjson(c.find({tenantId:"tenant-a",status:"paid"},{_id:0,createdAt:1,totalCents:1}).sort({createdAt:1}).hint("idx_esr").explain("executionStats"));

6. Avoid “one index per query” explosion

MongoDB allows many indexes, but the practical limit is usually reached before the hard collection limit because every index consumes disk/cache and must be maintained. If a compound index safely serves a leading-prefix query, a redundant prefix index may be removable—but uniqueness, sparsity/partial filtering, collation, and other properties can make apparently similar indexes semantically different. Do not drop a prefix index merely because the key names look redundant.

Migration discipline

Before removing a candidate prefix index: capture query shapes and baseline explains, inspect per-node usage, hide it, observe an appropriate workload window, verify no regression, then drop. Lesson 5 turns that into a complete lifecycle procedure.

7. Verification, cleanup, and production judgment

Verification checklist

  • All three candidate indexes exist with intentionally different field orders.
  • The same filter, projection, and sort are used for every forced comparison.
  • ESR and ERS are evaluated from execution evidence, not naming preference.
  • Prefix tests distinguish a leading-prefix seek from a forced scan of a weak/skipped prefix.
  • Ascending traversal is tested against the descending createdAt index direction.
  • No index is dropped solely because another index has a visually similar prefix.

Production judgment. Compound ordering is a workload contract. Equality fields usually lead; choose sort-before-range when avoiding a blocking sort matters, or range-before-sort when a highly selective range is more valuable. Limits, distributions, skew, and changing query shapes can reverse the winner. On replicas and shards, every added index has cluster-wide operational cost. Treat index consolidation as a change with observation and rollback—not a cleanup script. Lesson 3 adds arrays, where one document can generate multiple index keys and compound design gains additional hard constraints.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch10-l2docker volume rm atlasmart-mongo-ch10-l2-data

Check your understanding

  1. What does ESR stand for?
  2. Why can ERS be better than ESR?
  3. What is an index prefix?
  4. Can a query on the second compound field alone use the leading prefix efficiently?
  5. Why is hint useful in this lesson but risky as a production default?
Review the answers

1. Equality, Sort, Range.

2. A very selective range can reduce scanned keys enough to justify a later in-memory sort.

3. A beginning subset of the compound key sequence, such as {tenantId} or {tenantId,status}.

4. No. It lacks a bound on the leading field; a forced index may scan broadly and the planner may prefer another plan.

5. It isolates candidates for measurement, but hard-coding it can override future planner decisions when distributions/workloads change.

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.
  • ESR guideline — Equality-first guidance and the documented ESR-versus-ERS tradeoff.
  • Compound indexes — Prefix rules, field order, sort direction, and compound-index limits.

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.