📇 Indexing in MongoDB

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

Examples use the official MongoDB Node.js Driver and assume a collection named products.

📖 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.

Without Index
Scan document 1
Scan document 2
Scan document N (slow)

❓ 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

Every collection has an implicit index on _id by default, created automatically and impossible to remove.

💡 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

Indexes aren't free — each one adds write overhead and consumes disk and memory. Only index fields you actually query, sort, or join on.

đŸ› ī¸ 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 }); // descending

Tip

Use createIndexes() (plural) to build multiple indexes in a single call for efficiency.

🔤 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

Field order matters in compound indexes — follow the ESR rule: equality fields first, then sort fields, then range fields.

đŸ—ƒī¸ 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

You cannot create a compound index with two array fields — MongoDB disallows multiple multikey paths in the same index.

🔍 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

A collection can have at most one text index, though it can span multiple fields.

🌍 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

Hashed indexes don't support range queries efficiently, since sequential values hash to unrelated positions.

🃏 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

A background process removes expired documents roughly every 60 seconds — deletion isn't instantaneous at the exact expiry time.

🔒 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

Attempting to insert a duplicate value into a unique-indexed field throws a duplicate key error (code 11000).

🧩 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

This index only enforces uniqueness on documents that actually have an email field, allowing multiple documents without one.

✨ 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

Modern MongoDB generally recommends partial indexes over sparse indexes, since they offer more flexible filtering conditions.

🙈 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

PropertyEffect
uniqueDisallows duplicate values
sparseExcludes documents missing the field
partialFilterExpressionIndexes only matching documents
expireAfterSecondsAuto-deletes documents (TTL)
hiddenHides the index from the query planner
nameCustom 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

Building a large index on a production collection can be resource-intensive — consider building indexes during low-traffic windows or using rolling builds on replica sets.

⚡ 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

If totalDocsExamined is much higher than nReturned, the query is likely under-indexed for that filter pattern.

đŸŽ¯ 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

  1. Creating an index for every field "just in case," bloating write latency and memory usage.
  2. Getting compound index field order wrong, preventing efficient prefix matching.
  3. Forgetting that $regex queries without a leading anchor ^ can't use an index efficiently.
  4. Assuming an index exists is enough — never verifying with explain() that it's actually being used.
  5. Building large indexes on a busy production cluster without considering the performance impact.

Best Practice

Treat index design as an ongoing process — revisit indexes as query patterns evolve, not just once at schema design time.

❓ 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.

📌 26. Summary

>>"An index you never query is just overhead; the right index is the one your queries actually need."

Summary

Thoughtful indexing is the difference between a MongoDB application that scales and one that grinds to a halt under load. Continue exploring aggregation performance and transactions to round out your production readiness.