Introduction
Imagine your application has only 50 users during development. Everything works fine, and your database queries are fast.
A few months later, your application grows. Now, your users collection contains one million documents.
Suddenly, a simple login query starts taking several seconds:
db.users.find({ email: "rahim@example.com" }) The query looks simple, but MongoDB may need to check every document in the collection to find the matching email.
This is where indexing helps.
You can create an index on the email field with just one line:
db.users.createIndex({ email: 1 }) Now MongoDB can find the matching document without checking the entire collection.
In this article, we will look at how MongoDB indexes work, the different types of indexes, how to create compound indexes, and how to check whether an index is helping your queries.
All examples use mongosh with simple users and orders collections.
What Is a MongoDB Index?
Think about the index at the back of a textbook.
If you want to find a specific topic, you do not need to read every page. You check the index, find the page number, and go directly to that page.
MongoDB indexes work in a similar way.
An index is a data structure that stores field values in an organized order. MongoDB uses it to find documents without scanning the entire collection.
MongoDB uses B-tree indexes for most standard index types.
For example, if you frequently search users by email, you can create an index like this:
db.users.createIndex({ email: 1 }) One thing to remember: every MongoDB collection already has an index on the _id field.
You do not need to create that index yourself.
COLLSCAN vs IXSCAN
When MongoDB runs a query, it chooses a plan to find the matching documents.
Two stages are worth understanding:
COLLSCAN (Collection Scan)
MongoDB checks documents one by one until it finds the matching results.
This can become slow when a collection contains a large amount of data.
IXSCAN (Index Scan)
MongoDB searches through an index to find matching documents. It usually needs to examine far fewer documents.
You can check which approach MongoDB uses with explain().
db.users .find({ email: "rahim@example.com" }) .explain("executionStats") Before creating an index, the output might look like this:
{ "queryPlanner": { "winningPlan": { "stage": "COLLSCAN" } }, "executionStats": { "nReturned": 1, "totalDocsExamined": 1000000, "executionTimeMillis": 1200 } } After creating the index, you might see something like this:
{ "queryPlanner": { "winningPlan": { "stage": "FETCH", "inputStage": { "stage": "IXSCAN" } } }, "executionStats": { "nReturned": 1, "totalDocsExamined": 1, "executionTimeMillis": 2 } } Look at the difference.
Before indexing, MongoDB examined one million documents. After indexing, it examined only one.
These numbers are examples. Actual results depend on your data, hardware, and query.
Creating Your First Index
Creating an index in MongoDB is straightforward.
Use createIndex() and provide the field name with its direction.
-
1means ascending order. -
-1means descending order.
Here is a simple example:
// Create an index db.users.createIndex({ email: 1 }) // Check existing indexes db.users.getIndexes() // Remove an index db.users.dropIndex("email_1") MongoDB creates an index name automatically based on the field and direction.
For example:
{ email: 1 } Creates an index named:
email_1 For a single-field index, MongoDB can scan in either direction, so the direction usually does not make much difference.
Types of MongoDB Indexes
MongoDB supports several types of indexes. Each one is useful for a different kind of query.
Let's go through the ones you should know.
1. Single Field Index
A single-field index is created on one field.
It is useful when your application frequently searches or sorts documents using that field.
Example:
db.products.createIndex({ price: 1 }) Now MongoDB can use this index for queries that search or sort by price.
2. Compound Index
A compound index contains multiple fields.
You will often use this type of index in real applications because queries usually depend on more than one field.
Example:
db.orders.createIndex({ customerId: 1, createdAt: -1 }) This index can help queries that filter by customerId and sort by createdAt.
The order of fields matters.
Compound indexes also follow something called the prefix rule.
For example, consider this index:
db.users.createIndex({ name: 1, email: 1, age: 1 }) MongoDB can use this index for queries involving:
name-
nameandemail -
name,email, andage
However, it generally cannot use this index efficiently for a query that searches only by email or only by age.
Keep this in mind when designing compound indexes.
3. Multikey Index
A multikey index is used when a field contains an array.
MongoDB automatically creates index entries for the array elements.
You do not need to configure anything special.
For example, imagine a document like this:
{ title: "MongoDB 101", tags: ["database", "nosql", "beginner"] } You can create an index on the tags field:
db.posts.createIndex({ tags: 1 }) Now you can search for posts using an array element:
db.posts.find({ tags: "nosql" }) One important limitation: a compound multikey index can have at most one indexed array field in each document.
4. Text Index
A text index allows you to search string content in a collection.
It is useful for basic text searches.
Example:
db.posts.createIndex({ title: "text", body: "text" }) Now you can search for words in those fields:
db.posts.find({ $text: { $search: "indexing tutorial" } }) A collection can have only one text index, although that index can cover multiple fields.
For more advanced search features, such as typo tolerance and relevance tuning, you can look into MongoDB Atlas Search.
5. Unique Index
A unique index prevents duplicate values in a field.
It is useful for fields like email addresses, usernames, and other values that must be unique.
Example:
db.users.createIndex( { email: 1 }, { unique: true } ) Now MongoDB will not allow two documents to have the same email value.
This helps maintain data consistency at the database level.
6. TTL Index
A TTL (Time To Live) index automatically removes documents after a specified amount of time.
It is useful for:
- User sessions
- OTP records
- Temporary data
- Short-lived logs
For example, you can automatically delete session documents one hour after their creation time.
db.sessions.createIndex( { createdAt: 1 }, { expireAfterSeconds: 3600 } ) The indexed field must contain a date value.
One thing to remember: MongoDB does not delete expired documents at the exact second. Its background cleanup process runs periodically, usually around once a minute.
7. Partial Index
A partial index stores entries only for documents that match a specific condition.
This can reduce index size when you only need to query a certain group of documents.
For example, suppose you frequently search for active users.
Instead of indexing every user, you can create an index for active users only:
db.users.createIndex( { lastLogin: -1 }, { partialFilterExpression: { status: "active" } } ) MongoDB will include only documents where status is "active".
To use this index, your query must include a condition that matches the partial filter.
8. Sparse Index
A sparse index skips documents that do not contain the indexed field.
Example:
db.users.createIndex({ phone: 1 }, { sparse: true }) Documents without a phone field will not be included in this index.
Partial indexes offer more control, but you may still find sparse indexes in existing projects.
9. Hashed Index
A hashed index stores the hash of a field value instead of its original value.
It supports equality queries, but not range queries.
Example:
db.users.createIndex({ userId: "hashed" }) Hashed indexes are mainly useful in hashed sharding.
10. Geospatial Index
Geospatial indexes help MongoDB work with location-based data.
For example, you might need to find restaurants within 2 kilometers of a user's location.
You can create a 2dsphere index:
db.places.createIndex({ location: "2dsphere" }) Then search for nearby places:
db.places.find({ location: { $near: { $geometry: { type: "Point", coordinates: [90.4125, 23.8103] }, $maxDistance: 2000 } } }) The coordinates are longitude and latitude, and the distance is measured in meters.
11. Wildcard Index
Sometimes your documents contain dynamic fields, and you do not know in advance which fields users will search.
A wildcard index can help with these situations.
Example:
db.logs.createIndex({ "metadata.$**": 1 }) This creates an index for fields inside the metadata object.
However, do not use wildcard indexes as a replacement for properly designed compound indexes.
Compound Indexes and the ESR Rule
When creating compound indexes, the order of fields can have a big impact on query performance.
A useful rule to follow is ESR:
- E: Equality
- S: Sort
- R: Range
The general order is:
- Equality fields first.
- Sort fields next.
- Range fields last.
Let's look at an example.
Suppose you have this query:
db.users.find({ status: "active", age: { $gt: 25 } }).sort({ createdAt: -1 }) The query does three things:
- Finds users with an active status.
- Sorts the results by creation date.
- Returns users older than 25.
Following the ESR rule, you can create this index:
db.users.createIndex({ status: 1, createdAt: -1, age: 1 }) Why this order?
First, MongoDB uses status to narrow down the matching documents.
Next, it can read the matching entries in createdAt order from the index.
Finally, it applies the range condition on age.
This can help MongoDB avoid an additional in-memory sort.
The ESR rule is a useful starting point, but the best index still depends on your query and data.
Covered Queries
A covered query is a query where all the fields needed for filtering and returning results are available in an index.
This means MongoDB does not need to read the actual documents.
Let's look at an example.
First, create a compound index:
db.users.createIndex({ email: 1, name: 1 }) Now run this query:
db.users.find( { email: "rahim@example.com" }, { _id: 0, email: 1, name: 1 } ) The query uses email for filtering and returns email and name.
All these fields are available in the index.
We also excluded _id because MongoDB includes it by default, and it is not part of this index.
You can check whether a query is covered using:
.explain("executionStats") A covered query should show:
{ "totalDocsExamined": 0 } This means MongoDB did not need to examine any documents to return the result.
When Should You Create an Index?
Indexes can improve read performance, but they also have a cost.
Every index takes up disk space and memory.
MongoDB also needs to update indexes whenever documents are inserted, updated, or deleted.
So creating too many indexes can slow down write operations.
Here are some situations where an index is usually helpful.
Create an index when:
- A field is frequently used in query filters.
- A field is commonly used in
sort(). - A field is used as a join key in
$lookup. - A field has many different values, such as
emailororderId.
Be careful when:
- The collection contains only a few hundred documents.
- Your application performs many writes but very few reads.
- You are indexing a field with only a few possible values, such as
isActive. - An existing compound index already covers the queries you need.
For example, creating an index on a boolean field alone may not help much because there are only two possible values.
The important thing is to create indexes based on the queries your application actually runs.
Do not create an index on every field just because you can.
How to Read explain()
When a query becomes slow, do not guess what is happening.
Use explain("executionStats") to understand how MongoDB executes it.
Example:
db.orders .find({ customerId: 42 }) .sort({ createdAt: -1 }) .explain("executionStats") There are three things you should check.
1. winningPlan.stage
Look at the winning execution plan.
You will usually want to see IXSCAN instead of COLLSCAN for queries that benefit from an index.
A FETCH stage can also appear when MongoDB needs to read the matching documents after scanning the index.
2. totalDocsExamined vs nReturned
These two values help you understand how much work MongoDB is doing.
-
totalDocsExaminedtells you how many documents MongoDB examined. -
nReturnedtells you how many documents the query returned.
For example, if MongoDB examines 100,000 documents but returns only 5, your query may need a better index.
You should not expect these numbers to be identical for every query, but a large difference is worth investigating.
3. executionTimeMillis
This tells you how long the query took to execute.
You can use it to compare query performance before and after creating an index.
Also, look for a SORT stage in the execution plan.
It means MongoDB is performing an additional sort instead of reading the results in the required order from an index.
Managing Indexes in Production
Creating indexes is only part of the job.
You should also check which indexes your application actually uses.
Check Index Usage
MongoDB provides $indexStats to show how often indexes are used.
db.orders.aggregate([ { $indexStats: {} } ]) Look at the accesses.ops field.
It shows how many operations have used an index since the server was last started.
If an index has not been used, it may be worth reviewing.
However, do not remove an index based on usage statistics alone. Some indexes may only be needed for occasional reports or less frequent operations.
Hide an Index Before Removing It
If you are unsure whether an index is still needed, MongoDB allows you to hide it before deleting it.
This feature is available in MongoDB 4.4 and later.
A hidden index is still maintained by MongoDB, but the query planner will not use it.
You can test your application without that index and bring it back if needed.
// Hide an index db.orders.hideIndex("customerId_1_createdAt_-1") // Make the index available again db.orders.unhideIndex("customerId_1_createdAt_-1") This is safer than immediately deleting an index from a production database.
Check Index Size
You can also check how much space your indexes use.
db.orders.totalIndexSize() Or:
db.orders.stats().indexSizes Indexes that are frequently used should ideally fit in memory.
If they do not, MongoDB may need to read index data from disk more often, which can affect query performance.
Using Indexes with Mongoose
If you use Node.js and Mongoose, you can define indexes directly in your schemas.
For example:
const orderSchema = new mongoose.Schema({ customerId: { type: Number, required: true }, status: String, createdAt: { type: Date, default: Date.now } }); You can define a compound index like this:
orderSchema.index({ customerId: 1, createdAt: -1 }); For a unique email field:
const userSchema = new mongoose.Schema({ email: { type: String, unique: true } }); Mongoose can build indexes automatically when your application starts.
However, this may cause unnecessary load on a large production database.
For production applications, many teams disable automatic index creation and manage indexes through database migrations or deployment scripts.
You can disable automatic index creation like this:
mongoose.connect(uri, { autoIndex: false }); This gives you more control over when and how indexes are created.
Common Indexing Mistakes
Here are some mistakes developers often make when working with MongoDB indexes.
1. Creating an index on every field
More indexes do not always mean better performance. They use additional storage and can slow down write operations.
2. Using the wrong field order in compound indexes
The order of fields matters. Keep the ESR rule in mind when designing compound indexes.
3. Forgetting the prefix rule
An index on { a: 1, b: 1 } is useful for queries on a and on both a and b. It generally does not help a query that searches only by b.
4. Never checking explain()
Do not assume MongoDB is using your index. Check the execution plan and measure the results.
5. Keeping unused or duplicate indexes
For example, an index on { a: 1 } may be unnecessary if an index on { a: 1, b: 1 } already covers the same queries.
6. Expecting every regex query to use an index
A case-sensitive regex such as /^abc/ can use an index efficiently.
However, a regex like /abc/ generally cannot use an index in the same way.
7. Creating indexes directly on a busy production database
Always review the impact first. Test index changes in a staging environment before applying them to production.
MongoDB Indexing Checklist
Before considering your indexing work complete, go through this checklist:
- Have I checked slow queries using
explain("executionStats")? - Is MongoDB using
IXSCANwhere an index is useful? - Does my compound index follow the ESR rule?
- Is
totalDocsExaminedreasonable compared tonReturned? - Have I reviewed unused and duplicate indexes?
- Are my important indexes fitting in memory?
- Have I tested index changes before applying them to production?
Final Thoughts
MongoDB indexing is an important part of working with databases, especially when your application starts handling more users and data.
You do not need to memorize every index type from the beginning.
Start with a few basic habits:
- Check which queries are running slowly.
- Use
explain("executionStats")to understand their execution plans. - Create indexes based on your actual query patterns.
- Follow the ESR rule when creating compound indexes.
- Review your indexes from time to time.
These habits will help you understand where your database is spending time and how to improve query performance.
If you have worked with MongoDB indexing, I would like to hear about your experience.
Have you ever fixed a slow query by creating the right index?
Feel free to share your experience in the comments.






