Quick Answer

A MongoDB index is a sorted shortcut that lets the database jump straight to matching documents instead of scanning every one. Create them with db.collection.createIndex(), starting with the fields you filter or sort on most. Use explain() to confirm the query uses an IXSCAN, not a slow COLLSCAN. Indexes make reads much faster but slightly slow down writes, so add them only where they earn their keep.

Why MongoDB Indexes Speed Up Reads

If your queries feel slow as your data grows, the fix is almost always the same: indexes. This guide explains what MongoDB indexes are, how to create the three types you will use most, and how to prove an index is actually being used. No prior database-tuning experience needed.

Here is the core idea. Think of the index at the back of a textbook. Without it, finding every mention of "recursion" means reading all 500 pages one by one. With it, you flip to the index, read the page numbers, and jump straight there.

A MongoDB collection without an index behaves like the page-less book. To answer db.users.find({ email: "asha@example.com" }), the database has to look at every single document in the collection. That is called a collection scan (you will see it as COLLSCAN). On 200 documents it feels instant. On 2 million, it crawls. An index is a separate, sorted data structure (a B-tree) that stores your chosen field's values in order, each pointing to the matching document. Because it is sorted, MongoDB can binary-search it and jump to the answer without reading the rest.

QuestionNo index (COLLSCAN)Indexed (IXSCAN)
Fast reads on large data?NoYes
Fastest possible writes?YesSlightly slower
Uses extra storage and RAM?NoYes, some
Documents scanned per query?Every oneOnly matches

Creating a Single-Field Index

The simplest and most common index covers one field. You create it with createIndex(), passing an object that names the field and a direction: 1 for ascending, -1 for descending.

// In the mongosh shell
db.users.createIndex({ email: 1 })

That one line tells MongoDB to build a sorted index on the email field of the users collection. Now a lookup like this is fast even with millions of users:

db.users.find({ email: "asha@example.com" })

When to reach for one: any field you frequently filter on (find), sort by, or match in a range. Good candidates are things like email, username, userId, or createdAt.

Does direction matter? For a single-field index, not much — MongoDB can walk the index both ways, so 1 and -1 both serve a simple sort. Direction only starts to matter with compound indexes, which we cover next.

One field you never need to index yourself is _id. MongoDB creates a unique index on _id automatically for every collection.

Compound Indexes: More Than One Field

Real queries often filter on several fields at once, or filter on one field and sort by another. A compound index covers multiple fields in a single structure. List the fields in the order that matters for your queries:

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

This index is perfect for a common request: "show me one customer's orders, newest first."

db.orders
  .find({ customerId: 42 })
  .sort({ orderDate: -1 })

The gotcha most beginners hit is field order. A compound index can only be used from left to right, like a phone book sorted by last name, then first name. The index above serves queries that filter on:

  • customerId alone, or
  • customerId and orderDate together.

But it does not efficiently serve a query that filters on orderDate alone. That would be like trying to find everyone named "Asha" in a phone book sorted by last name first — the field you want is not the one it is organised by. This left-to-right rule is called the index prefix.

A simple way to order the fields is the ESR rule: put Equality matches first, then the field you Sort on, then Range filters ($gt, $lt) last. Following ESR usually gives the most useful index for the widest set of queries.

Unique Indexes: Enforcing No Duplicates

Sometimes an index is not just about speed — it is about correctness. A unique index makes MongoDB reject any document that would duplicate an existing value. This is the clean way to guarantee, say, that no two accounts share an email.

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

The second argument, { unique: true }, is an options object. After this, inserting a second user with an email that already exists throws a duplicate key error instead of quietly creating a copy.

Two things to watch for:

  • Existing duplicates block creation. If the collection already contains two documents with the same email, MongoDB refuses to build the unique index. Clean up the duplicates first, then create it.
  • Missing fields count as null. A document with no email field is treated as having email: null, and only one such document is allowed. If many documents may lack the field, add { unique: true, sparse: true } so only documents that have the field are checked.

You can make compound indexes unique too — for example { studentId: 1, courseId: 1 } with unique: true stops a student from enrolling in the same course twice.

Confirming an Index With explain()

Creating an index is only half the job. You still need to check that your query actually uses it — a small typo or the wrong field order can leave the index sitting unused. The tool for this is explain(). Pass "executionStats" to see what really happened when the query ran:

db.users
  .find({ email: "asha@example.com" })
  .explain("executionStats")

You do not need to read the whole output. Look at two things. First, the winningPlan stage:

"winningPlan": {
  "stage": "FETCH",
  "inputStage": {
    "stage": "IXSCAN",
    "keyPattern": { "email": 1 },
    "indexName": "email_1"
  }
}

IXSCAN means your index was used — good. If you instead see "stage": "COLLSCAN", the query scanned the whole collection and your index did not help.

Second, compare two numbers in executionStats:

"executionStats": {
  "nReturned": 1,
  "totalDocsExamined": 1,
  "totalKeysExamined": 1
}

You want totalDocsExamined to be close to nReturned. Returning 1 document after examining 1 is ideal. Returning 1 after examining 50,000 means the query is doing far too much work and the index is not really covering it.

The Write-Cost Trade-Off

Indexes are not free, and this is the part beginners most often overlook. Every index is a live copy of your data that MongoDB must keep in sync. So on every insert, update that touches an indexed field, or delete, MongoDB updates the collection and each affected index.

Put simply:

  • Indexes make reads faster.
  • Indexes make writes a little slower and use extra disk and RAM.

One handy index is a great trade. Ten overlapping indexes on a write-heavy collection can quietly slow down every insert and eat memory for indexes that queries rarely touch. The best-performing indexes also need to fit in RAM; if they spill to disk, you lose much of the benefit.

Rule of thumb: add an index because a real, frequent query needs it — not "just in case." Index the fields your app actually filters and sorts on, and stop there.

If you are unsure whether an index is worth its cost, that is exactly what explain() and your slow-query logs are for. Measure, then decide.

Managing Indexes and Next Steps

Two commands cover day-to-day index housekeeping. List every index on a collection, including the automatic _id one:

db.users.getIndexes()

And drop an index you no longer need, either by its key pattern or by the name shown in getIndexes():

// by key pattern
db.users.dropIndex({ email: 1 })

// or by name
db.users.dropIndex("email_1")

A clear starting recommendation for beginners:

  1. Index the field in your most common find() filter (a single-field index).
  2. Add a unique index for fields that must not repeat, like email or username.
  3. When you filter-and-sort together, build one compound index using the ESR order.
  4. Run explain("executionStats") and confirm you see IXSCAN, not COLLSCAN.
  5. Remove indexes no queries use, so writes stay fast.

Get comfortable with those five steps and you will have solved the great majority of MongoDB speed problems. To practise on real collections and go deeper into aggregation, schema design, and queries, work through the free MongoDB course on Priodemy.

Frequently Asked Questions

Do MongoDB indexes update automatically when I change data?

Yes. Once an index exists, MongoDB keeps it in sync for you. Every insert, delete, or update to an indexed field automatically updates the index too. You never refresh it by hand — that automatic upkeep is exactly why writes get a little slower on heavily indexed collections.

How do I know if my query is actually using an index?

Run the query with .explain("executionStats") and look at the winning plan's stage. IXSCAN means an index was used; COLLSCAN means MongoDB scanned the whole collection. Also compare nReturned with totalDocsExamined — they should be close. A huge gap means the index is not really helping.

How many indexes are too many?

There is no magic number, but each extra index slows writes and uses more storage and RAM. A good habit is to index only the fields your app frequently filters or sorts on, and to remove indexes no query uses. If a collection has many overlapping indexes that explain() never picks, that is a sign you have too many.

Does the order of fields in a compound index matter?

Yes, a lot. A compound index is used left to right (the "prefix" rule). An index on { a: 1, b: 1 } serves queries on a, or on a and b together, but not queries on b alone. A helpful ordering guide is the ESR rule: Equality fields first, then the Sort field, then Range filters.

What is the difference between a unique index and a normal index?

A normal index only speeds up reads. A unique index does that and enforces a rule: no two documents may share the same value for that field. You create it with db.collection.createIndex({ email: 1 }, { unique: true }). Any insert or update that would create a duplicate is rejected with a duplicate key error.

Should I index the _id field?

No need. MongoDB automatically creates a unique index on _id for every collection the moment it is created, so lookups by _id are already fast. You only create indexes for the other fields your queries filter or sort on.