MongoDB Indexes: A Practical Guide to Faster Queries
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.
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.
- Equality fields first (exact matches like
status: "shipped") - Sort fields next
- Range fields last (
$gt,$lt,$inover 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:
- winningPlan.stage:
IXSCANis good;COLLSCANmeans 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.










