Blog

Indexing Types | MongoDB Indexing Tutorial

Key Takeaways

  • Choose indexes from actual query patterns: filters, sort order, and returned fields.
  • Single-field and compound indexes organize ordered keys; arrays, text, geographic data, hashed keys, and dynamic fields need different indexing behavior.
  • Indexes consume storage and cache, and writes must maintain affected index entries. More indexes aren't automatically better.
  • Validate candidates with execution statistics. Hiding an index tests query-planner behavior but doesn't remove its maintenance costs or constraints.

MongoDB index types support different access patterns, from exact lookups and array membership to text and geographic searches. Choose the smallest useful set of indexes that serves your application's queries—not an index for every field.

What an index does—and what it costs

An index maintains searchable keys associated with documents. Ordinary ascending and descending indexes organize values in order, helping suitable filters and sorts; other MongoDB index types have different key-generation and query rules.

An index can reduce the documents examined, but the planner may still choose a collection scan. A predicate matching most documents has low selectivity—it eliminates little work—and an index may not help enough to justify using it.

Index storage isn't simply a second copy of the collection. Cost depends on indexed values, entry counts, compression, and index count; inserts, deletes, and changes to indexed values also require maintenance. For related walkthroughs, see creating MongoDB indexes and evaluating index performance.

MongoDB index types with examples

Run this setup once in a fresh, disposable indexing_types_lab database. Subsequent examples share these collections. The small dataset demonstrates syntax, not performance; don't reproduce every demonstration index in production.

db = db.getSiblingDB("indexing_types_lab");
db.users.insertMany([
  {
    _id: ObjectId(),
    name: "Sam Erickson", email: "sam@example.com", age: 42,
    address: {
      city: "New York", state: "NY", zip: "10001",
      deliveryZone: "north"
    },
    roles: ["admin", "user", "author", "originator"],
    location: {
      type: "Point", coordinates: [-73.856077, 40.848447]
    },
    currLoc: [12, 8], createdAt: new Date()
  },
  {
    _id: ObjectId(),
    name: "Erica", email: "erica@example.com", age: 30,
    address: { city: "Boston", state: "MA", zip: "02108" },
    roles: ["user"],
    location: { type: "Point", coordinates: [-71.06, 42.36] },
    currLoc: [15, 10], createdAt: new Date()
  }
]);

Single-field index

A single-field index indexes one field. Specify 1 for ascending or -1 for descending order; an ordinary single-field index can be traversed in either direction.

db.users.createIndex({ name: 1 });
db.users.find({ name: "Sam Erickson" });
db.users.find().sort({ name: -1 });
db.users.find({ name: "Sam Erickson", email: "sam@example.com" });

The name index alone doesn't evaluate the email predicate. For embedded data, distinguish whole-document equality from querying a nested field:

db.users.createIndex({ address: 1 });
db.users.createIndex({ "address.city": 1 });
db.users.find({ "address.city": "New York" });

The city query needs the dotted-path index, not the whole-address index.

When to use a single-field index

Use it for recurring lookups, selective ranges, or sorting on one field. Check whether an existing compound index already provides the needed leading field.

Compound index

A compound index orders multiple fields together. Field order determines its useful prefixes—the leading sequence of fields.

db.users.createIndex(
  { name: 1, email: 1, age: -1 },
  { name: "name_email_age" }
);
db.users.find({ name: "Sam Erickson" });
db.users.find({ name: "Erica", email: "erica@example.com", age: 30 });
db.users.find({ name: "Sam Erickson", age: { $gt: 30 } })
  .sort({ email: 1 });
Index: name ASC -> email ASC -> age DESC

name                  leading-field lookup
name + email          leading-prefix lookup
name + email + age    full-key lookup
name + age            usable name prefix; potentially wider scan
email or age alone    no leading-prefix lookup

With name fixed by equality, the index can provide { email: 1, age: -1 } or its complete reverse, { email: -1, age: 1 }, but not { email: 1, age: 1 }. See the official index sorting rules.

When to use a compound index

Match recurring multi-field filters and sorts. Start with the Equality, Sort, Range guideline; test range-before-sort when a highly selective range substantially reduces scan work. Our compound-index guide expands on prefixes.

Multikey index

Indexing an array field automatically produces multikey behavior, with index entries for its distinct values.

db.users.createIndex({ roles: 1 });
db.users.find({ roles: "admin" });

A compound multikey index can't index two independent array fields in the same document. For arrays of objects, use $elemMatch when conditions must match the same element; array queries also have special index-bound and coverage rules.

When to use a multikey index

Use it for frequent array-membership queries, such as finding users with a role. Large arrays can generate many index entries.

Text index

A text index supports $text queries. A collection can have one text index, which may include multiple fields.

db.users.createIndex({ name: "text", email: "text" });
db.users.find({ $text: { $search: "Erickson" } });
// Alternative definition, not an additional text index:
// db.users.createIndex({ name: "text" });

Text matching uses language-dependent stemming and stop words, not arbitrary substring matching. Indexing email as text doesn't replace an exact email lookup index.

When to use a text index

Use it when built-in word search meets your requirements. In Atlas, evaluate MongoDB Search for configurable analyzers, autocomplete, and faceting; its search indexes are distinct from a classic text index.

Wildcard index

Wildcard indexes support changing field names within a document or subtree:

db.users.createIndex({ "address.$**": 1 });
db.users.find({ "address.deliveryZone": "north" });

Scope them narrowly. Broad indexing adds storage and write work, and doesn't make an unselective predicate selective. Under the wildcard restrictions, a wildcard term supports only one query predicate, not every dynamic-field condition together.

When to use a wildcard index

Use it when queried field names aren't predictable. Prefer targeted indexes for stable, high-volume query shapes.

2dsphere index

Geospatial indexes distinguish spherical geometry from flat coordinates. For GeoJSON locations on Earth, use 2dsphere:

db.users.createIndex({ location: "2dsphere" });
db.users.find({
  location: {
    $near: {
      $geometry: {
        type: "Point", coordinates: [-73.856077, 40.848447]
      },
      $maxDistance: 5000
    }
  }
});

Coordinates are longitude first, then latitude. This $near query returns results within 5,000 meters, nearest first; $geoWithin supports containment without distance ordering.

When to use a 2dsphere index

Use it for nearby places, service areas, and other spherical geographic queries.

2d index

A 2d index works with legacy coordinate pairs interpreted on a flat plane. Here, currLoc represents local [x, y] coordinates, not a GeoJSON location.

db.users.createIndex({ currLoc: "2d" });
db.users.find({
  currLoc: { $geoWithin: { $box: [[0, 0], [20, 20]] } }
});

When to use a 2d index

Use it for planar coordinate queries. Don't substitute it for 2dsphere when you need accurate spherical geometry.

Hashed index

A hashed index indexes hashes of field values:

db.users.createIndex({ name: "hashed" });
db.users.find({ name: "Sam Erickson" });

It can support equality access and hashed shard keys, but not ordered range access or sorting on the hashed field's original values. Creating this index doesn't shard or rebalance the collection.

When to use a hashed index

Evaluate it as part of hashed shard-key design. Assess cardinality, distribution, and hotspots; hashing a low-cardinality field doesn't create more distinct values.

Atlas Vector Search index

In Atlas, a Vector Search index supports similarity searches over embeddings—numeric vectors representing content—through $vectorSearch. Manage it through search-index APIs such as createSearchIndex(), not an ordinary createIndex({ embedding: 1 }).

When to use a Vector Search index

Use it for semantic similarity rather than exact value lookup. Configure the vector field, dimensions, similarity function, and any pre-filter fields in its definition; validate retrieval quality as well as resource cost.

Index properties: constraints, subsets, and lifecycle

These describe index options and behavior, not interchangeable key types. Combinations have restrictions: for example, hashed indexes can't be unique, and TTL indexes are single-field.

Unique indexes

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

A unique index prevents duplicate keys across documents. Compound uniqueness applies to the combination, not each field independently. An ordinary unique single-field index permits only one null index entry, including documents where the field is missing.

For ranged sharded collections, unique indexes generally need the complete shard key as a prefix. The default _id index only enforces uniqueness per shard when _id isn't the shard key.

When to use a unique index

Use it for database-enforced identity rules. Check existing duplicates, nulls, and missing values before building; see our unique-index walkthrough.

Partial indexes

A partial index contains only documents satisfying its filter:

db.users.createIndex(
  { email: 1, name: 1 },
  { partialFilterExpression: { age: { $gt: 30 } } }
);
// Eligible: this age condition implies the partial filter.
db.users.find({ email: "sam@example.com", age: { $gt: 40 } });
// Not eligible for this partial index:
db.users.find({ email: "sam@example.com" });

Eligibility doesn't guarantee selection. The second query could need documents excluded by the filter, so this partial index can't provide complete results.

When to use a partial index

Use it when important queries consistently target a subset. It can reduce index storage and maintenance compared with indexing the entire collection.

Sparse indexes versus partial indexes

A sparse single-field index skips missing fields but includes explicit null values. Partial indexes offer more precise control:

db.contacts.insertMany([
  { email: "sam@example.com" },
  { email: null },
  { label: "email missing" }
]);
db.contacts.createIndex(
  { email: 1 }, { name: "email_sparse", sparse: true }
);
db.contacts.createIndex(
  { email: 1 },
  {
    name: "email_strings",
    partialFilterExpression: { email: { $type: "string" } }
  }
);
db.contacts.find({ email: { $type: "string", $eq: "sam@example.com" } });
db.contacts.find({ email: null });

The sparse index includes the string and explicit null; the partial index includes only the string. The null query also matches the missing field, so forcing the sparse index would omit a matching document.

When to use a sparse index

Use it for straightforward field-presence indexing. Prefer partial indexes when eligibility depends on type, status, or another predicate.

TTL indexes

A time-to-live index makes documents eligible for background deletion relative to an indexed date:

db.sessions.insertOne({ user: "sam", createdAt: new Date() });
db.sessions.createIndex(
  { createdAt: 1 }, { expireAfterSeconds: 60000 }
);

This document becomes eligible when its createdAt date is 60,000 seconds old—not necessarily 60,000 seconds after insertion. Deletion is asynchronous, not an exact scheduling guarantee; the indexed value must contain a BSON date or qualifying date array.

When to use a TTL index

Use it for retention cleanup of sessions, logs, or temporary data. Enforce authorization expiry in application checks rather than waiting for deletion.

Hidden indexes

A hidden index is unavailable to the query planner but remains maintained and consumes storage and memory.

db.users.createIndex({ age: 1 }, { name: "age_hidden", hidden: true });
db.users.hideIndex("name_1");
// Observe representative workload before restoring the index.
db.users.unhideIndex("name_1");

When to use a hidden index

Hide an existing index to assess query-plan regressions before dropping it. This doesn't measure the storage or write savings of actually removing it.

Choose indexes from query shapes, then validate them

  1. Record real filters, sorts, projections, and result limits. A projection specifies which fields the query returns.
  2. Measure selectivity with representative values, including unusually common values.
  3. Design candidates around prefixes and ordering. Index intersection can combine scans from multiple indexes, but don't assume the planner will choose it instead of a purpose-built compound index.
  4. Compare execution work and application latency, then account for write and storage costs.

For background on candidate enumeration, MongoDB's planner implementation includes index-intersection planning.

Read explain execution statistics

Compare a forced collection-scan baseline with an indexed plan:

db.users.find({ name: "Sam Erickson" })
  .hint({ $natural: 1 }).explain("executionStats");
db.users.find({ name: "Sam Erickson" })
  .hint("name_1").explain("executionStats");

Then remove hint() to inspect the planner's choice. In the explain results, inspect:

  • nReturned: documents returned.
  • totalKeysExamined: index entries scanned.
  • totalDocsExamined: document examinations.
  • queryPlanner.winningPlan: the selected plan, sometimes with a nested queryPlan. IXSCAN denotes an index scan; COLLSCAN denotes a collection scan. Exact plan structure varies by execution engine.

Look for unnecessary scan work relative to results—not merely the presence of an index scan.

Check whether the query is covered

A covered query gets everything it needs from one index without fetching documents. Filtering, sorting, and returned fields all matter. This example excludes _id because it isn't in our compound index:

db.users.find(
  { name: "Sam Erickson" },
  { _id: 0, name: 1, email: 1, age: 1 }
).sort({ email: 1, age: -1 })
  .hint("name_email_age")
  .explain("executionStats");

For this nonempty covered result, expect zero documents examined and no document-fetch stage.

Production Checklist

  • Inventory existing definitions before adding indexes; our guide to listing indexes covers inspection.
  • Respect the database index limits: 64 indexes per collection and 32 fields per compound index. Keep keys compact and array fan-out bounded.
  • Budget disk, cache, CPU, and write amplification. Populated-collection builds also require memory and temporary disk space.
  • Follow the index-build guidance. In MongoDB 8.0, builds take exclusive collection locks at their start and end; the old background: true option is ignored.
  • Before removal, hide and observe over a representative workload, including scheduled jobs. Read-use counts alone don't establish that an index is unnecessary: also check uniqueness and TTL responsibilities.
  • Review existing expiration backlogs before adding TTL. A newly built TTL index can trigger substantial deletion work.

On an authorized self-managed deployment, inspect builds from another shell with $currentOp. This all-users example requires the inprog privilege:

db.getSiblingDB("admin").aggregate([
  { $currentOp: { allUsers: true, idleConnections: true } },
  { $match: { $or: [
    { "command.createIndexes": { $exists: true } },
    { op: "none", msg: /^Index Build/ }
  ] } }
]);

FAQ

What are the different types of indexes in MongoDB?

Common MongoDB index types include single-field, compound, multikey, text, wildcard, 2dsphere, 2d, and hashed indexes. Search and Vector Search use separate search indexes; unique, partial, sparse, TTL, and hidden describe properties or behaviors with compatibility restrictions.

How do I choose between a single-field and compound index?

Use a single-field index when one field serves the important lookup or sort. Choose a compound index when recurring queries need multiple fields together, and order its fields around useful prefixes, equality conditions, sorting, and ranges.

What is the difference between a sparse index and a partial index?

A sparse single-field index omits documents missing that field but includes explicit null values. A partial index includes only documents matching its filter, and a query must imply that filter for the index to be eligible.

How can I tell whether MongoDB is using an index?

Run the query with explain("executionStats") and inspect the winning plan for an index-access stage. Compare totalKeysExamined and totalDocsExamined with nReturned; an index scan alone doesn't prove the query is efficient.

Does a MongoDB TTL index delete documents exactly when they expire?

No. A TTL index makes documents eligible for asynchronous deletion based on the indexed date and expireAfterSeconds, so expired documents can remain until the background process removes them.

Further Reading

Written by Sam Erickson

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