Every MongoDB developer eventually hits the same wall: queries that were fast with a few thousand test documents suddenly crawl once a collection grows to millions of records in production. Almost always, the fix comes down to indexing — and more specifically, understanding which kind of index actually matches how your application queries the data. This guide covers the major index types in MongoDB, how they work internally, and how to design an indexing strategy that scales with your application rather than fighting against it.
Why Indexes Matter
Without an index, MongoDB has to perform a collection scan — examining every single document in a collection to find the ones matching a query. For a collection with a handful of documents, this is instant. For a collection with tens of millions of documents, this can take seconds or longer, and that latency multiplies with every concurrent user running that same query.
An index is a separate data structure — in MongoDB’s case, typically a B-tree — that stores a sorted, efficiently searchable reference to a subset of fields, pointing back to the full documents. Instead of scanning the entire collection, MongoDB can traverse the index to find matching documents almost instantly, then fetch only those specific documents.
The tradeoff, as with indexes in any database, is that they consume additional disk space and add overhead to write operations — every insert, update, or delete needs to update every relevant index, not just the underlying document. This is why indexing strategy isn’t “add as many indexes as possible” — it’s about identifying exactly which fields your application actually queries, sorts, or filters on, and indexing those deliberately.
The Default Index: _id
Every MongoDB collection automatically has a unique index on the _id field, created the moment the collection itself is created. This is what makes lookups by _id — the most common query pattern in most applications — fast by default, without any manual configuration.
db.books.getIndexes()
// [ { v: 2, key: { _id: 1 }, name: '_id_' } ]
Single Field Indexes
The simplest index type indexes exactly one field, in either ascending (1) or descending (-1) order.
// Create an ascending index on the "author" field
db.books.createIndex({ author: 1 })
// Create a descending index on "published_date"
db.books.createIndex({ published_date: -1 })
For a single-field index used for equality or range matching, the sort direction (1 vs -1) generally doesn’t matter — MongoDB can traverse a B-tree index in either direction efficiently. The direction becomes important when combined with a sort operation matching that direction, or when used within a compound index (more on that below).
// This query benefits directly from the index on "author"
db.books.find({ author: "Robert C. Martin" })
// So does this range query on "published_date"
db.books.find({ published_date: { $gte: ISODate("2020-01-01") } })
When to Use Single Field Indexes
Single field indexes are the right choice when a field is frequently queried on its own — user lookups by email, product lookups by SKU, order lookups by status. They’re the most common and often the first indexes added to any collection beyond the default _id index.
Compound Indexes
A compound index spans multiple fields, and it’s where indexing strategy starts to require real thought, because the order of fields in a compound index fundamentally determines which queries it can efficiently support.
db.orders.createIndex({ customer_id: 1, order_date: -1, status: 1 })
This index supports queries filtering on customer_id alone, customer_id and order_date together, or all three fields together — but it does not efficiently support a query filtering only on order_date or only on status, because of how B-tree indexes work internally.
The ESR Rule: Equality, Sort, Range
A widely used mental model for ordering fields in a compound index is the ESR rule — Equality fields first, then Sort fields, then Range fields.
// Query pattern: filter by exact customer_id (equality),
// sort by order_date (sort), filter by a price range (range)
db.orders.find({
customer_id: "cust123",
total: { $gte: 50, $lte: 500 }
}).sort({ order_date: -1 })
// The ideal compound index for this query:
db.orders.createIndex({ customer_id: 1, order_date: -1, total: 1 })
Placing the equality field (customer_id) first lets MongoDB narrow down to a small, contiguous section of the index immediately. Placing the sort field (order_date) second means MongoDB can return results in the requested order directly from the index, without an additional, expensive in-memory sort step. Placing the range field (total) last works because range conditions are less selective for index traversal purposes than equality conditions, and putting them earlier in the index can actually prevent MongoDB from using later fields in the index effectively.
The “Prefix” Rule
A compound index can serve any query that uses a prefix of its fields, in order, but not queries that skip over fields in the middle.
// Compound index: { a: 1, b: 1, c: 1 }
// Can use the index efficiently:
db.collection.find({ a: 1 })
db.collection.find({ a: 1, b: 2 })
db.collection.find({ a: 1, b: 2, c: 3 })
// Cannot use the index efficiently (skips field "a"):
db.collection.find({ b: 2 })
db.collection.find({ c: 3 })
// Can partially use the index (uses "a", but not "c" directly):
db.collection.find({ a: 1, c: 3 })
This prefix behavior is why field order in a compound index matters so much, and why it’s worth deliberately designing compound indexes around your most common and most performance-critical query shapes, rather than adding fields to an index arbitrarily.
Multikey Indexes
MongoDB automatically creates a multikey index when you index a field that contains an array — no special syntax is required, MongoDB detects the array and indexes each element individually.
// A document with an array field
{ title: "Clean Code", genres: ["software", "career", "best-seller"] }
// Indexing the array field
db.books.createIndex({ genres: 1 })
// This query benefits from the multikey index
db.books.find({ genres: "career" })
Internally, MongoDB creates one index entry per array element, so a document with 5 genres creates 5 separate index entries pointing back to the same document. This works well for querying array membership but comes with an important restriction: a compound index can have at most one array field, because indexing the Cartesian product of multiple array fields would cause index size to explode combinatorially.
Text Indexes
MongoDB supports basic full-text search through text indexes, useful for simple keyword search without needing a dedicated search engine like Elasticsearch for lighter workloads.
db.books.createIndex({ title: "text", description: "text" })
db.books.find({ $text: { $search: "pragmatic programming" } })
// Sort by relevance score
db.books.find(
{ $text: { $search: "pragmatic programming" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })
A collection can only have one text index (though it can span multiple fields), and text search in MongoDB, while useful for basic keyword matching, doesn’t offer the sophistication of a dedicated search platform — for advanced relevance tuning, fuzzy matching, or faceted search, most production systems eventually pair MongoDB with Atlas Search or an external search engine.
Geospatial Indexes
Geospatial indexes support location-based queries — finding points within a certain radius, within a bounding shape, or sorted by proximity to a given location. MongoDB supports two types.
2dsphere Indexes
Used for data stored using GeoJSON format, and the standard choice for real-world geospatial data on a sphere (i.e., Earth).
// A document with a GeoJSON point
{
name: "Coffee Shop",
location: {
type: "Point",
coordinates: [74.3436, 31.5497] // [longitude, latitude]
}
}
db.places.createIndex({ location: "2dsphere" })
// Find places within 2km of a given point
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [74.3436, 31.5497] },
$maxDistance: 2000
}
}
})
// Find places within a polygon (e.g., a defined city boundary)
db.places.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [[[74.30, 31.50], [74.40, 31.50], [74.40, 31.60], [74.30, 31.60], [74.30, 31.50]]]
}
}
}
})
Note the coordinate order — GeoJSON specifies [longitude, latitude], which is the reverse of how coordinates are often casually written, and a very common source of bugs for developers new to geospatial data.
2d Indexes (Legacy)
The older 2d index type is used for simple flat-plane coordinate data rather than true spherical geometry, and is largely considered legacy at this point — new applications should use 2dsphere unless there’s a specific reason to model coordinates on a flat plane rather than a sphere.
Other Specialized Index Types
- TTL (Time-To-Live) Indexes: Automatically delete documents after a specified number of seconds, based on a date field. Extremely useful for session data, temporary tokens, or logs that should expire automatically.
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
- Unique Indexes: Enforce that no two documents can have the same value for the indexed field(s), useful for fields like email addresses or usernames.
db.users.createIndex({ email: 1 }, { unique: true })
- Partial Indexes: Index only documents matching a specified filter condition, reducing index size and overhead when you only need to query a subset of documents efficiently.
db.orders.createIndex(
{ customer_id: 1 },
{ partialFilterExpression: { status: "pending" } }
)
- Sparse Indexes: Only include documents that actually have the indexed field, skipping documents where the field is missing entirely — useful for optional fields present in only a subset of documents.
db.users.createIndex({ phone_number: 1 }, { sparse: true })
- Wildcard Indexes: Index an unknown or dynamic set of fields, useful for applications with highly variable document structures where you can’t predict every field in advance.
db.products.createIndex({ "attributes.$**": 1 })
Analyzing Index Performance with explain()
Before and after adding an index, it’s essential to verify it’s actually being used the way you expect.
db.orders.find({ customer_id: "cust123", status: "shipped" }).explain("executionStats")
Key things to check in the output:
winningPlan.stage: Look forIXSCAN(index scan) rather thanCOLLSCAN(full collection scan) — aCOLLSCANon a large collection is almost always a sign that an index is missing or not being used.totalDocsExaminedvs.nReturned: Ideally these numbers are close to each other. A huge gap (examining a million documents to return ten) suggests the index isn’t selective enough for the query, or the wrong index is being used.executionTimeMillis: The actual time taken — useful for comparing before and after adding an index.
Real-World Use Cases
- E-commerce product search: Compound indexes on category, price, and rating support common filter-and-sort browsing patterns; text indexes support basic product name search.
- Ride-sharing and delivery apps: 2dsphere indexes power “find nearby drivers” or “find nearby restaurants” queries central to the entire product experience.
- Session and token management: TTL indexes automatically clean up expired sessions or password reset tokens without needing a separate cleanup job.
- User authentication systems: Unique indexes on email or username fields enforce data integrity at the database level, not just in application code.
- Analytics dashboards: Compound indexes aligned with common date-range-plus-filter query patterns keep dashboard queries fast even as underlying event data grows into the billions of documents.
Advantages of a Well-Designed Indexing Strategy
- Dramatically faster query performance, often the difference between milliseconds and seconds (or worse) at scale.
- Reduced server resource consumption, since indexed queries examine far fewer documents than full collection scans.
- Enforced data integrity through unique indexes, without needing application-level uniqueness checks that are vulnerable to race conditions.
- Automatic data lifecycle management through TTL indexes, reducing the need for separate cleanup scripts or cron jobs.
Limitations and Challenges
- Write performance overhead: Every index must be updated on every write, so collections with heavy write volume and many indexes can see meaningfully slower insert/update performance.
- Storage overhead: Indexes consume disk space, and in extreme cases, a collection’s total index size can approach or exceed the size of the data itself.
- Compound index field order sensitivity: A poorly ordered compound index provides little to no benefit for many query shapes, and it’s a common source of “why isn’t my index being used” confusion.
- Index selection limits: MongoDB’s query planner picks one index per query (with limited exceptions), so having many narrow, overlapping indexes doesn’t necessarily help and can waste resources.
Security Considerations
While indexing itself isn’t primarily a security feature, a few related considerations matter:
- Unique indexes on sensitive identifiers (like email) help prevent duplicate account creation, which can be a vector for abuse if not enforced.
- Avoid indexing sensitive fields unnecessarily — every indexed field is duplicated into the index structure, meaning sensitive data indexed carelessly effectively exists in two places, which can complicate data deletion and compliance obligations (like GDPR erasure requests) if not accounted for.
- Monitor index usage for query patterns that might indicate scraping or abuse — unusually broad range queries or repeated full-text searches at high volume can sometimes signal automated abuse of an application’s search functionality.
Scalability Considerations
As collections grow into the tens or hundreds of millions of documents, indexing strategy has direct implications for horizontal scaling via sharding. The shard key you choose is very often backed by an index (usually a compound index), and a poorly chosen shard key can lead to uneven data distribution across shards regardless of how well individual queries are indexed.
For very large collections, it’s also worth monitoring whether your working set of frequently accessed indexes fits comfortably in available RAM — MongoDB’s performance benefits from indexes drop off significantly if the index itself is too large to be cached in memory and has to be read from disk repeatedly.
Best Practices
- Index based on actual query patterns, verified with
explain(), not guesswork about what “might” be queried. - Follow the ESR rule (Equality, Sort, Range) when designing compound indexes.
- Avoid over-indexing. Every unused index is pure write overhead and storage cost with no query benefit — periodically review and drop indexes that aren’t being used (
db.collection.aggregate([{ $indexStats: {} }])shows usage statistics). - Use partial and sparse indexes to keep index size down when you only need to index a subset of documents.
- Always specify correct coordinate order (
[longitude, latitude]) for geospatial data to avoid subtle, hard-to-debug location bugs. - Use TTL indexes for genuinely expiring data rather than building custom cleanup jobs.
- Re-evaluate indexing strategy as query patterns evolve — an index design that fit your application at launch may not fit it a year later as new features and query patterns are added.
Conclusion
Indexing is one of the highest-leverage skills a MongoDB developer or DBA can master — the difference between a well-indexed and poorly-indexed collection often isn’t a modest performance improvement, it’s the difference between an application that scales gracefully and one that grinds to a halt as data grows. Single field indexes cover the basics, compound indexes (designed carefully around the ESR rule and your real query patterns) handle more complex filtering and sorting, and specialized index types like geospatial, text, and TTL indexes extend MongoDB’s capabilities into location-based search, keyword search, and automatic data lifecycle management. The common thread across all of them: design indexes around how your application actually queries data, verify with explain(), and revisit that strategy as your application evolves.