Indexes are special data structures that store a small, ordered slice of a collection's data, allowing MongoDB to find matching documents without scanning every document in the collection. Choosing the right indexes is one of the highest-leverage things you can do for application performance. This tutorial covers every major index type, how to manage them, and how to verify they're actually being used.
Information
đ 1. Introduction
Without an index, MongoDB must perform a collection scan â reading every document to determine whether it matches a query. As collections grow into the millions of documents, this becomes prohibitively slow. Indexes solve this by maintaining a sorted structure that queries can search efficiently.
â 2. What are Indexes?
An index is conceptually similar to an index at the back of a book â instead of reading every page, you jump directly to the relevant section. In MongoDB, indexes are implemented as B-tree structures (with some exceptions like hashed and geospatial indexes).
Note
đĄ 3. Why Use Indexes?
- Dramatically speed up queries that filter, sort, or join on indexed fields.
- Support efficient sorting without an in-memory sort step.
- Enable covered queries, which avoid reading full documents entirely.
- Enforce uniqueness constraints on fields like emails or SKUs.
Caution
đ ī¸ 4. Creating Indexes
Use createIndex() to build a new index on a collection. The method accepts a key pattern describing the field(s) and sort direction.
Creating a basic index
await collection.createIndex({ price: 1 }); // ascending
await collection.createIndex({ createdAt: -1 }); // descendingTip
đ¤ 5. Single Field Indexes
The simplest index type, built on a single field. Supports queries and sorts on that field in either direction.
Single field index
await collection.createIndex({ category: 1 });
// Efficiently supported by the index above
await collection.find({ category: "electronics" }).toArray();đ§Š 6. Compound Indexes
Indexes multiple fields together, supporting queries that filter or sort on any prefix of the indexed fields.
Compound index and prefix matching
await collection.createIndex({ category: 1, price: -1 });
// Uses the full index
await collection.find({ category: "electronics" }).sort({ price: -1 }).toArray();
// Uses only the "category" prefix of the index
await collection.find({ category: "electronics" }).toArray();Important
đī¸ 7. Multikey Indexes
Automatically created when you index a field containing an array. MongoDB indexes each array element individually.
Multikey index on an array field
await collection.createIndex({ tags: 1 });
// Matches any document whose "tags" array contains "sale"
await collection.find({ tags: "sale" }).toArray();Warning
đ 8. Text Indexes
Enables efficient full-text search across string fields, supporting stemming and relevance scoring.
Creating and querying a text index
await collection.createIndex({ name: "text", description: "text" });
const results = await collection
.find({ $text: { $search: "wireless mouse" } })
.toArray();Note
đ 9. Geospatial Indexes
Supports queries on location data, including proximity searches and geometric containment checks.
2dsphere index and a proximity query
await collection.createIndex({ location: "2dsphere" });
const nearby = await collection.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [80.2707, 13.0827] },
$maxDistance: 5000
}
}
}).toArray();#ī¸âŖ 10. Hashed Indexes
Stores the hash of a field's value rather than the value itself, commonly used as a shard key to distribute writes evenly.
Creating a hashed index
await collection.createIndex({ userId: "hashed" });Caution
đ 11. Wildcard Indexes
Indexes all fields (or a subset via a pattern) in a document, useful for collections with unpredictable or dynamic schemas.
Wildcard index on a nested field
await collection.createIndex({ "metadata.$**": 1 });âŗ 12. TTL Indexes
Time-To-Live indexes automatically delete documents after a specified number of seconds past a date field â ideal for sessions, logs, and caches.
Creating a TTL index
await collection.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // expires after 1 hour
);Tip
đ 13. Unique Indexes
Enforces that no two documents can share the same value for the indexed field(s).
Enforcing uniqueness
await collection.createIndex({ email: 1 }, { unique: true });Danger
đ§Š 14. Partial Indexes
Indexes only the documents that satisfy a specified filter expression, reducing index size and improving write performance.
A partial unique index
await collection.createIndex(
{ email: 1 },
{ unique: true, partialFilterExpression: { email: { $exists: true } } }
);Example
⨠15. Sparse Indexes
Excludes documents that are missing the indexed field entirely, similar in spirit to a partial index but simpler.
Creating a sparse index
await collection.createIndex({ discountCode: 1 }, { sparse: true });Note
đ 16. Hidden Indexes
Hidden indexes remain in the collection and stay up to date, but are invisible to the query planner â useful for safely testing whether an index is truly necessary before dropping it.
Creating and toggling a hidden index
await collection.createIndex({ legacyField: 1 }, { hidden: true });
// Toggle visibility later
await db.command({ collMod: "products", index: { name: "legacyField_1", hidden: false } });âī¸ 17. Index Properties
| Property | Effect |
|---|---|
| unique | Disallows duplicate values |
| sparse | Excludes documents missing the field |
| partialFilterExpression | Indexes only matching documents |
| expireAfterSeconds | Auto-deletes documents (TTL) |
| hidden | Hides the index from the query planner |
| name | Custom index name |
đ§° 18. Index Management
Managing existing indexes
// List all indexes
const indexes = await collection.listIndexes().toArray();
// Drop a specific index
await collection.dropIndex("category_1_price_-1");
// Drop all indexes except _id
await collection.dropIndexes();Caution
⥠19. Query Optimization
The MongoDB query planner automatically selects the most efficient index for a given query based on cached statistics and index selectivity.
Hinting a specific index
// Force a specific index if needed
await collection.find({ category: "electronics" }).hint({ category: 1 }).toArray();đ§ 20. Explain Plans
Use explain() to inspect which index (if any) a query used, and how many documents were examined versus returned.
Inspecting index usage
const plan = await collection
.find({ category: "electronics" })
.explain("executionStats");
console.log(plan.queryPlanner.winningPlan.inputStage.indexName);
console.log(plan.executionStats.totalDocsExamined);Example
đ¯ 21. Covered Queries
A covered query is one where every field in the query filter and projection exists within the index itself, allowing MongoDB to skip reading the actual documents entirely.
A covered query
await collection.createIndex({ category: 1, price: 1 });
// Covered: filter and projection fields are both in the index
const results = await collection
.find({ category: "electronics" }, { projection: { category: 1, price: 1, _id: 0 } })
.toArray();đ 22. Index Performance
- Follow the ESR rule when designing compound indexes.
- Avoid creating redundant indexes that are prefixes of an existing compound index.
- Monitor index usage with $indexStats to find and remove unused indexes.
- Keep frequently used indexes small enough to fit comfortably in RAM.
Checking index usage statistics
const stats = await collection.aggregate([{ $indexStats: {} }]).toArray();
console.log(stats);đ 23. Best Practices
- Design indexes around your actual query patterns, not hypothetical future ones.
- Use explain() during development to confirm expected index usage before shipping.
- Prefer a small number of well-chosen compound indexes over many overlapping single-field indexes.
- Regularly audit and remove unused indexes to reduce write overhead.
- Use partialFilterExpression to keep indexes small when only a subset of documents matter.
â ī¸ 24. Common Mistakes
- Creating an index for every field "just in case," bloating write latency and memory usage.
- Getting compound index field order wrong, preventing efficient prefix matching.
- Forgetting that $regex queries without a leading anchor ^ can't use an index efficiently.
- Assuming an index exists is enough â never verifying with explain() that it's actually being used.
- Building large indexes on a busy production cluster without considering the performance impact.
Best Practice
â 25. Frequently Asked Questions
Yes, slightly. Every insert, update, or delete must also update each relevant index, so more indexes mean more write overhead.
Index builds in modern MongoDB versions are non-blocking by default, allowing reads and writes to continue during construction.
A single collection can have up to 64 indexes, though far fewer are typically needed in practice.