Indexes and Performance

Creating indexes, compound indexes, explain(), and common MongoDB anti-patterns.

Why indexes matter in MongoDB too

Just like a relational database, MongoDB scans every document in a collection to satisfy a query unless an index lets it jump directly to matching documents. On a small collection this is invisible; on a collection with millions of documents, an unindexed query on a frequently-filtered field is the single most common cause of a slow MongoDB application.

Javascript
db.products.createIndex({ category: 1 });

1 means ascending order, -1 descending — for a single-field index used only for equality filtering, the direction rarely matters, but it matters for sorting (covered below). MongoDB automatically creates an index on _id for every collection; every other field needs an explicit index if it's queried often.

Compound indexes

A compound index covers multiple fields in one structure, most useful when queries commonly filter (or sort) on more than one field together:

Javascript
db.orders.createIndex({ userId: 1, status: 1 });

Just like a relational composite index, field order matters — MongoDB's compound indexes follow the same leftmost-prefix principle covered in the MySQL indexing page. An index on { userId: 1, status: 1 } efficiently serves:

Javascript
// Uses the full index
db.orders.find({ userId: 42, status: "shipped" });

// Uses the index too (prefix: just userId)
db.orders.find({ userId: 42 });

But it does not efficiently serve a query filtering on status alone, for the same reason a MySQL composite index can't: the index is sorted by userId first, so without a userId value there's no efficient way to jump to matching status values.

explain() — checking whether an index is actually used

explain() shows MongoDB's query execution plan, the direct equivalent of SQL's EXPLAIN:

Javascript
db.orders.find({ userId: 42 }).explain("executionStats");

Before an index exists, the relevant part of the output shows a full collection scan:

Javascript
{
  executionStats: {
    executionSuccess: true,
    nReturned: 3,
    totalDocsExamined: 9482,
    executionStages: {
      stage: "COLLSCAN"   // collection scan: every document was examined
    }
  }
}

totalDocsExamined: 9482 against nReturned: 3 is the tell — 9,482 documents inspected to return only 3. After db.orders.createIndex({ userId: 1 }):

Javascript
{
  executionStats: {
    executionSuccess: true,
    nReturned: 3,
    totalDocsExamined: 3,
    executionStages: {
      stage: "FETCH",
      inputStage: {
        stage: "IXSCAN",   // index scan: jumped straight to matching documents
        indexName: "userId_1"
      }
    }
  }
}

IXSCAN confirms the index was used, and totalDocsExamined dropping to match nReturned means MongoDB examined only the documents it actually returned — the same signal EXPLAIN's rows/type columns give in MySQL.

Common anti-patterns

Unbounded array growth

An array field that grows without bound (comments, activity log entries, notifications pushed onto one document) slows down every read and write against that document as it grows, and risks the 16MB per-document size limit outright — this is the same concern raised on the schema-design page, but it directly affects performance too: MongoDB must read and rewrite the entire document on most updates, so a document that keeps growing gets progressively more expensive to touch even for unrelated field updates.

Javascript
// Anti-pattern: this array has no natural ceiling
{ _id: ObjectId("post1"), title: "...", comments: [ /* could grow to thousands */ ] }

The fix, per the schema-design page, is referencing (a separate comments collection) once growth is unbounded, or capping the embedded array to a bounded "recent N" summary.

Missing indexes on real query fields

The most common real-world MongoDB performance problem is simply querying on a field with no supporting index — easy to miss because MongoDB doesn't refuse to run the query; it just falls back to COLLSCAN silently and gets slower as the collection grows, with no error to flag it.

Indexing everything

Just as in a relational database, every index makes writes slower (each insert/update must update every affected index) and consumes additional storage. db.collection.getIndexes() lists a collection's current indexes; $indexStats (via the aggregation pipeline) shows how often each index is actually used, which surfaces indexes safe to drop.

Common mistakes

  • Assuming a compound index on { a: 1, b: 1 } speeds up every query touching a or b — like MySQL, it only helps queries filtering on a alone or on a and b together, not b alone.
  • Letting an embedded array grow without bound and only noticing the performance impact once documents are already large in production.
  • Never running explain() on a slow query and instead guessing at the cause — COLLSCAN vs IXSCAN in the output settles the question immediately.
  • Creating indexes reactively per-query without ever reviewing getIndexes()/$indexStats, leading to redundant or unused indexes quietly taxing every write.