Compound indexes and the ESR rule

MongoDB · Practical · 12 min

Most production queries filter on several fields and sort, and field order decides whether an index helps.

A compound index such as { status: 1, createdAt: -1 } is sorted by status first and, within each status, by createdAt. Like a phone book sorted by last then first name, it supports queries on any prefix: { status } alone or { status, createdAt }, but not { createdAt } alone.

Order fields with the ESR rule:

  • Equality fields first (status: "paid", userId: 42),
  • then Sort fields (createdAt: -1),
  • then Range fields (total: { $gt: 100 }, $in with many values behaves like a range for sorting).

With equality fields first, all matching entries are contiguous and already in sort order, so MongoDB avoids an in-memory SORT stage. Putting a range field before the sort field breaks that ordering.

Direction matters only between fields: { a: 1, b: -1 } can serve sort { a: 1, b: -1 } or its reverse { a: -1, b: 1 }, but not { a: 1, b: 1 }.

A query is covered when the filter and projection use only indexed fields (and exclude _id unless it's indexed): the answer comes straight from the index without fetching documents, shown as no FETCH stage in explain.

One well-designed compound index often replaces several single-field ones, because its prefixes serve those queries too.

Example

db.orders.createIndex({ customerId: 1, createdAt: -1, total: 1 })

// E (customerId) + S (createdAt) + R (total): one IXSCAN, no SORT stage
db.orders.find({ customerId: 42, total: { $gt: 100 } }).sort({ createdAt: -1 })

// prefix use: also served by the same index
db.orders.find({ customerId: 42 })

// not a prefix: cannot use this index efficiently
db.orders.find({ createdAt: { $gte: ISODate("2026-01-01") } })

// covered query: only indexed fields, _id excluded, no FETCH stage
db.orders.find({ customerId: 42 }, { _id: 0, createdAt: 1, total: 1 })

Remember

  • Compound indexes serve queries on a prefix of their fields
  • Order fields Equality, Sort, Range
  • Direction matters only relative to other fields in the index
  • Covered queries are answered from the index alone

Watch out for

  • Putting the range field before the sort field, which forces an in-memory sort.

Check yourself

  1. Query: find({ userId: 7, score: { $gt: 50 } }).sort({ date: -1 }). Which index follows ESR?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/