Blog

Introduction to MongoDB Indexing

Key Takeaways

  • MongoDB indexes can reduce the documents a query examines, but they consume storage and add write-maintenance costs.
  • Creating an index doesn't guarantee MongoDB will use it. Verify the selected plan and examined keys/documents with explain("executionStats").
  • Compound-index field order determines which filters and sorts an index can serve efficiently.
  • Choose indexes for actual query patterns and data distribution, then monitor their value before adding or removing more.

MongoDB indexing improves query performance by giving the database an efficient path to matching documents instead of scanning an entire collection. The tradeoff is additional storage and work on writes, so the goal is a small set of useful indexes—not an index on every field.

What Is a MongoDB Index?

An index stores selected field values alongside references to their documents. MongoDB uses a B-tree structure for its indexes: an ordered structure that makes it possible to locate values without checking every document. Ordinary ascending and descending indexes can support equality filters, range filters, and compatible sorts.

In the regular, unsharded collections used here, MongoDB automatically creates a unique index on _id. That index prevents duplicate document identifiers, but it doesn't accelerate queries on unrelated fields. The MongoDB index overview explains these fundamentals.

Collection scan:
query a = 3 --> inspect each document --> matching documents

Indexed lookup:
query a = 3 --> locate matching index keys --> fetch documents

The selected execution plan determines which path actually runs.

Start with the operations your application performs. Index design and MongoDB schema design belong in the same conversation.

Create an Index and Verify the Query Plan

Run these examples in mongosh, connected to your deployment. Use a fresh scratch database named stackchief_indexing_demo; these are shell examples, not Node.js driver code. Run the setup once so existing indexes or duplicate sample data don't change the comparison.

// Run in mongosh, using a fresh scratch database.
db = db.getSiblingDB("stackchief_indexing_demo");

db.collection.insertMany([
  { a: 1, b: "ab" },
  { a: 6, b: "no" },
  { a: 2, b: "cd" },
  { a: 4, b: "jk" },
  { a: 3, b: "ef" },
  { a: 5, b: "lm" }
]);

db.collection.find({ a: 3 });
db.collection.find({ a: 3 }).explain("executionStats");

The insert generates _id values automatically. Without a supporting index on a, this query must inspect the collection's documents to find every match.

Now create an ascending index and inspect the same query again:

db.collection.createIndex({ a: 1 });
db.collection.find({ a: 3 }).explain("executionStats");

The 1 specifies ascending index-key order. It doesn't sort the query's results automatically. For more creation examples, see our MongoDB create index tutorial.

Read the Plan and the Work Performed

Use the explain results reference to inspect the selected plan, including nested stages:

  • COLLSCAN means a collection scan.
  • IXSCAN means an index scan; recent releases can also show optimized indexed stages such as EXPRESS_IXSCAN.
  • FETCH means MongoDB retrieves documents after locating index entries.
  • executionStats.nReturned counts returned documents.
  • executionStats.totalKeysExamined counts examined index entries, while totalDocsExamined counts document examinations.

Compare examined keys and documents with the number returned. An index scan that examines a large portion of an index may still be expensive. This six-document example demonstrates the mechanism, not a meaningful latency benchmark.

MongoDB Index Types

Single-Field Indexes

The index on a is a single-field index. Use one when queries primarily filter or sort on that field. The default _id index is another example.

Compound Indexes and Leading Prefixes

A compound index contains multiple fields in a defined sequence:

db.collection.createIndex({ a: 1, b: -1 });

// Matching index order.
db.collection.find({}).sort({ a: 1, b: -1 });

// Equality on the leading field, then sorting within that group.
db.collection.find({ a: 3 }).sort({ b: -1 });

// No leading-field condition: not an efficient b-only lookup.
db.collection.find({ b: "ef" }).explain("executionStats");

This index orders entries by a ascending, then by b descending within each a value. Its leading prefix is { a: 1 }, so it can support queries on a alone as well as queries on both fields. Don't expect it to replace an index designed for b-only queries.

A compound index can also support the complete reverse of its key order. For this index, that means { a: -1, b: 1 }, not { a: 1, b: 1 }. See MongoDB's compound-index sort rules.

Multikey Indexes for Arrays

A multikey index indexes array contents. MongoDB marks an index as multikey when an indexed field contains an array; it does not automatically create an index for every array field.

An index can be both compound and multikey. For example, an index on scalar category and array-valued tags has both properties. In a compound multikey index, each document can have at most one indexed field whose value is an array.

Geospatial Indexes

Use 2dsphere for geographic queries on an Earth-like sphere and 2d for flat, legacy coordinate data. For geographic GeoJSON points, coordinates are ordered [longitude, latitude], not the reverse. MongoDB's geospatial query guide covers the required data formats and operators.

A built-in text index supports the $text operator. A collection can have one text index, although it can include multiple fields. Its configured language controls suffix stemming and stop words; the default language is English.

For autocomplete, faceting, and configurable relevance ranking, evaluate MongoDB Search, also familiar to developers as Atlas Search. It uses separate search indexes and the $search aggregation stage, not the built-in text index.

Deployment scope matters: Atlas manages the search process for you. On supported self-managed deployments, you operate the separate mongot process yourself; review the self-managed search deployment documentation before choosing that path.

Hashed Indexes

A hashed index stores hashes of field values and can support equality lookups. It cannot support range lookups on the hashed field because hashing doesn't preserve the original value order. MongoDB's hashed-index creation guide explains that limitation.

Hashed indexes commonly support hashed shard keys, but creating one does not shard your collection. Choosing a shard key is a separate data-distribution decision.

Wildcard Indexes

Wildcard indexes support queries on unpredictable or changing field names. They're useful for genuinely dynamic attributes, not as a shortcut around query analysis. Prefer targeted indexes when you know the fields your application queries.

Design Compound Indexes Around Your Queries

The Equality-Sort-Range guideline, or ESR, is a useful starting point: put equality-filter fields first, then sort fields, then range-filter fields.

For example, consider an endpoint that retrieves open orders for one tenant, filters by minimum total, and sorts newest first:

// Continue in the same mongosh scratch database.
db.orders.insertMany([
  { tenantId: "t1", status: "open", createdAt: new Date("2026-10-01T12:00:00Z"), total: 120 },
  { tenantId: "t1", status: "open", createdAt: new Date("2026-10-02T12:00:00Z"), total: 80 },
  { tenantId: "t1", status: "closed", createdAt: new Date("2026-10-03T12:00:00Z"), total: 200 }
]);

db.orders.createIndex(
  { tenantId: 1, status: 1, createdAt: -1, total: 1 },
  { name: "tenant_status_recent_total" }
);

db.orders.find({
  tenantId: "t1",
  status: "open",
  total: { $gte: 100 }
}).sort({ createdAt: -1 }).explain("executionStats");

The equality fields select a tenant/status group; the next key provides the requested date order. The trailing field represents the range predicate. However, ESR isn't a law: a highly selective range may make Equality-Range-Sort faster even if MongoDB must then sort the smaller result set.

Compare candidate orders using representative data and the real sort and limit requirements. A global newest-orders query has a different shape from this tenant-scoped query. Our MongoDB compound index tutorial explores field order further.

You can sometimes avoid fetching documents entirely. A covered query obtains its filter and returned fields from an index. The { a: 1, b: -1 } index can cover this scalar-field example because the projection excludes _id:

db.collection.find(
  { a: 3 },
  { _id: 0, a: 1, b: 1 }
).explain("executionStats");

For a covered execution, totalDocsExamined is zero. Learn how to select returned fields in our MongoDB projections guide.

Useful Index Properties: Unique, Partial, Sparse, and TTL

These properties change an index's behavior or which documents it includes; they aren't interchangeable alternatives to compound or multikey indexes.

PropertyWhen to use itImportant constraint
UniqueEnforce a business rule such as unique account identifiers.Existing duplicate keys prevent a unique-index build. A regular single-field unique index also permits only one null/missing index entry.
PartialIndex a useful subset, such as open orders.A query must imply the partial filter for MongoDB to use it without omitting results.
SparseExclude documents missing an indexed field.A single-field sparse index still includes explicit null values. Partial indexes provide more filtering control.
TTLAutomatically remove expiring sessions or temporary records.TTL indexes are single-field, date-based indexes; deletion is asynchronous, not an exact expiration deadline.

For an open-orders-only workload, this partial index is another candidate to test:

db.orders.createIndex(
  { tenantId: 1, createdAt: -1 },
  {
    name: "open_orders_recent",
    partialFilterExpression: { status: "open" }
  }
);

// Eligible because the query includes the partial filter.
db.orders.find({ tenantId: "t1", status: "open" })
  .sort({ createdAt: -1 });

For per-document expiration timestamps, a TTL index can use expireAfterSeconds: 0:

db.sessions.createIndex(
  { expiresAt: 1 },
  { expireAfterSeconds: 0 }
);

Store expiresAt as a BSON date, not a date string. Check expiration in application reads when expired records must no longer be usable; don't rely on immediate TTL deletion.

Performance: Selectivity, Storage, and Cache Pressure

Cardinality means the number of distinct values. Selectivity describes how narrowly a query filters the collection. A Boolean field has few distinct values, but querying its rare value can still be selective. Measure the distribution of the values you actually query, as illustrated in MongoDB's selectivity guide.

Use uniqueness for correctness, not as a performance switch. Adding unique: true doesn't make an unsuitable query selective; it rejects duplicate indexed keys.

There is no useful universal minimum index size. Size depends on indexed values, index structure, and storage representation. Inspect actual sizes rather than budgeting a fixed number of kilobytes per index.

Nor must every index fit in RAM for a workload to perform well. WiredTiger uses both its internal cache and the operating system's filesystem cache. Focus on the active working set—the data and index pages accessed repeatedly—and correlate cache pressure and disk reads with query latency.

Every additional index also creates maintenance work. Inserts and deletes affect applicable index entries; updates to indexed values require corresponding index changes. A write-heavy workload makes unnecessary indexes especially costly.

Inspect, Build, and Remove Indexes Safely

Inventory Definitions, Sizes, and Usage

db.collection.getIndexes();
db.collection.stats().indexSizes;
db.collection.aggregate([{ $indexStats: {} }]);

getIndexes() lists index definitions. stats().indexSizes reports per-index sizes; $indexStats reports usage, not size. See the official collection statistics reference and our guide to listing MongoDB indexes.

In $indexStats output, inspect accesses.ops, accesses.since, and the host. Counters are node-local and reset after events such as a server restart or index recreation. Observe all relevant read-serving nodes and a representative business cycle before declaring an index unused.

Treat Index Builds as Production Changes

Test builds against realistic data, budget disk and memory headroom, and monitor write latency, disk activity, and replication lag. Modern builds allow interleaved reads and writes during most of the build but take exclusive collection locks at the beginning and end. The old background option is ignored; it isn't a safety mechanism.

Validate uniqueness before building a unique index, and schedule expensive builds during lower load. MongoDB's index-build documentation covers locking and replicated deployment behavior.

Hide Before Dropping

A hidden index remains maintained but is unavailable to the query planner. This lets you evaluate removal while retaining the option to unhide it.

In the scratch example, a_1 is a redundancy candidate because the compound index has a as its leading prefix. Start the experiment with:

db.collection.hideIndex("a_1");

Observe representative traffic and query plans. If performance regresses, restore visibility:

db.collection.unhideIndex("a_1");

Alternatively, after a successful observation period and a review of constraints and dependencies, remove the candidate:

// Only after validating that removal is safe.
db.collection.dropIndex("a_1");

Common Mistakes

  • Indexing fields instead of query shapes: record each important filter, sort, projection, and limit before proposing an index.
  • Testing only one parameter value: compare common and rare values, small and large tenants, and realistic result sizes.
  • Dropping an index solely because its usage counter is zero: review uniqueness, expiration, shard-key dependencies, and infrequent operational queries first.

FAQ

How do I check whether MongoDB is using an index?

Run the query with explain("executionStats") and inspect the winning plan, including nested stages. IXSCAN or an optimized indexed stage indicates index access; COLLSCAN indicates a collection scan. Compare totalKeysExamined and totalDocsExamined with nReturned to assess the work performed.

What is the difference between a compound index and a multikey index?

A compound index contains multiple fields; a multikey index indexes array values. An index can be both compound and multikey, but each document can have at most one array-valued indexed field in a compound multikey index.

Does MongoDB automatically create indexes on array fields?

No. You create an index on the field, and MongoDB automatically makes that index multikey when the indexed field contains an array.

How many indexes can a MongoDB collection have?

The current MongoDB server manual documents a maximum of 64 indexes per collection, including the default _id index. That is a ceiling, not a recommended index count; check the versioned manual for your deployment.

How do indexes affect MongoDB write performance?

Inserts and deletes maintain applicable index entries, and updates to indexed values require corresponding index changes. Additional indexes therefore add write work and storage costs; measure those costs against the read performance they provide.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me