MediumMongoDB Indexes: A Practical Guide to Faster Queries | by Sahil Khaire | Nov, 2026 | Medium
Sitemap
Sahil Khaire

Sahil Khaire

Software engineer writing practical guides on backend, databases, and the tools I use every day. Clear explanations, real code, no fluff.

MongoDB Indexes: A Practical Guide to Faster Queries

5 min read3 days ago

--

Press enter or click to view image in full size

Why your queries are slow, and how the right index fixes them

If your MongoDB app was fast at launch and crawls now, the cause is usually not the hardware. It is missing or poorly designed indexes. This guide covers how indexes work, the types you will use most, and how to tell whether a query is using one.

What is an index?

Without an index, MongoDB runs a collection scan (COLLSCAN): it reads every document to find the matches. That is fine for 1,000 documents and painful for 10 million.

An index is a sorted data structure (a B-tree) that stores the values of one or more fields plus pointers to the documents. MongoDB can walk the tree to the matching entries instead of reading everything, much like using the index at the back of a book.

Press enter or click to view image in full size

Every collection gets one index for free: the unique index on _id.

Creating your first index

db.users.createIndex({ email: 1 })

The 1 means ascending; -1 means descending. For a single field the direction barely matters, because MongoDB can traverse the index either way. It matters for compound indexes, covered below.

To see existing indexes:

db.users.getIndexes()

Types of indexes

1. Single field index

Indexes one field. Good for queries that filter or sort on that field.

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

2. Compound index

Indexes several fields in a specific order. This is the index you will use most in real apps.

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

A compound index supports queries on its prefixes. The index above serves queries on:

  • { customerId }
  • { customerId, status }
  • { customerId, status, createdAt }

It does not efficiently serve a query on status alone, because status is not a prefix.

3. Multikey index

When you index a field that holds an array, MongoDB creates an index entry for each element automatically. No special syntax is needed.

// tags: ["mongodb", "database", "nosql"]
db.posts.createIndex({ tags: 1 })
db.posts.find({ tags: "mongodb" })

4. Text index

Supports basic keyword search over string content. A collection can have only one text index, though it can cover several fields.

db.articles.createIndex({ title: "text", body: "text" })
db.articles.find({ $text: { $search: "index performance" } })

For relevance ranking, fuzzy matching, or autocomplete at scale, look at Atlas Search instead.

5. Geospatial index

Use 2dsphere for location queries such as "find restaurants near me".

db.places.createIndex({ location: "2dsphere" })
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [77.59, 12.97] },
$maxDistance: 2000
}
}
})

6. Hashed index

Stores a hash of the field value. Mainly used as a shard key to spread writes evenly. It supports equality matches but not range queries.

db.events.createIndex({ userId: "hashed" })

7. Wildcard index

Indexes fields whose names you cannot predict, such as user-defined attributes.

db.products.createIndex({ "attributes.$**": 1 })

Index properties

Unique: rejects duplicate values.

db.users.createIndex({ email: 1 }, { unique: true })

Partial: indexes only documents matching a filter. Smaller index, faster writes.

db.orders.createIndex(
{ createdAt: -1 },
{ partialFilterExpression: { status: "active" } }
)

Sparse: skips documents that lack the field. Partial indexes are more flexible and generally preferred.

Get Sahil Khaire’s stories in your inbox

Join Medium for free to get updates from this writer.

TTL: deletes documents automatically after a set time. Ideal for sessions, logs, and OTPs.

db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

The ESR rule for compound indexes

Field order in a compound index matters a lot. The most useful guideline is ESR: Equality, Sort, Range.

Press enter or click to view image in full size
  1. Equality fields first (exact matches like status: "shipped")
  2. Sort fields next
  3. Range fields last ($gt, $lt, $in over many values)

Take this query:

db.orders.find({ status: "shipped", amount: { $gt: 500 } })
.sort({ createdAt: -1 })

The ESR-friendly index is:

db.orders.createIndex({ status: 1, createdAt: -1, amount: 1 })

Putting the range field before the sort field would force MongoDB to sort results in memory, which is slow and capped by memory limits.

Covered queries

If every field a query filters on and returns is in the index, MongoDB answers from the index alone and never touches the documents. That is a covered query, and it is about as fast as reads get.

db.users.createIndex({ email: 1, name: 1 })
db.users.find({ email: "a@b.com" }, { name: 1, _id: 0 })

Note the _id: 0. Since _id is not in this index, returning it would break coverage.

Is my query using an index? Use explain()

db.orders.find({ status: "shipped" }).explain("executionStats")

Check these fields:

Press enter or click to view image in full size
  • winningPlan.stage: IXSCAN is good; COLLSCAN means no index was used.
  • totalKeysExamined and totalDocsExamined compared with nReturned: ideally all three are close. Examining 100,000 documents to return 10 means the index is not selective enough.
  • SORT stage: an in-memory sort. Usually fixable by reordering the index per ESR.

The cost of indexes

Indexes are not free:

  • Slower writes: every insert, update, and delete must update each index.
  • Memory: indexes perform best when they fit in RAM alongside your working set.
  • Storage: each index takes disk space.

Find indexes that are never used and drop them:

db.orders.aggregate([{ $indexStats: {} }])
db.orders.dropIndex("status_1")

Before dropping an index in production, consider hiding it first with db.orders.hideIndex("status_1"). The planner ignores a hidden index, and you can unhide it instantly if performance drops.

Best practices checklist

  • Index based on your real query patterns, not on every field.
  • Prefer one well-designed compound index over several single-field ones.
  • Follow ESR when ordering compound index fields.
  • Run explain() on your most frequent and slowest queries.
  • Turn on the profiler or check Atlas Performance Advisor to find slow queries.
  • Use partial and TTL indexes to keep indexes small.
  • Remove unused indexes; they slow every write.
  • Build large indexes during low traffic and test on staging first.

Wrapping up

Indexes are the single biggest lever for MongoDB performance. Start from your queries, design compound indexes with ESR in mind, verify with explain(), and prune what you do not use. A few well-chosen indexes can turn a multi-second query into a few milliseconds.

If this helped, follow for more practical database and backend guides.

--

--

Written by Sahil Khaire

Software engineer writing practical guides on backend, databases, and the tools I use every day. Clear explanations, real code, no fluff.