An index only helps if the query planner chooses it, and the only way to know is to ask. explain() shows the plan MongoDB picked and, with execution statistics, how much work it did. The database profiler does the same job in reverse: it records the operations that were slow in production so you know which queries to explain. After this lesson you will be able to read a plan stage by stage, judge it from three numbers, force or verify index usage, and locate slow operations on a live server.
| Mode | What you get | Runs the query? |
|---|---|---|
| explain() / "queryPlanner" | the winning plan and rejected plans | no |
| explain("executionStats") | plus counters for the winning plan | yes |
| explain("allPlansExecution") | plus counters for every candidate plan | yes |
executionStats is the everyday choice.
A plan is a tree; each stage feeds the one above it through inputStage. Read it from the innermost stage outwards. On servers using the slot-based execution engine the tree sits under winningPlan.queryPlan.
// Without a suitable index
{ stage: "SORT", sortPattern: { createdAt: -1 },
inputStage: { stage: "COLLSCAN", filter: { status: { $eq: "open" } } } }
// With { status: 1, createdAt: -1 }
{ stage: "FETCH", filter: { total: { $gt: 100 } },
inputStage: { stage: "IXSCAN", indexName: "status_1_createdAt_-1",
indexBounds: { status: ['["open", "open"]'], createdAt: ["[MaxKey, MinKey]"] } } }| Stage | Meaning |
|---|---|
| COLLSCAN | read every document; fine for tiny collections, a red flag otherwise |
| IXSCAN | walk an index within indexBounds |
| FETCH | load full documents for the keys found |
| SORT | in-memory sort (blocking, 100 MB limit) |
| PROJECTION_COVERED | result built from index keys alone: no FETCH |
| SORT_MERGE / OR | combine several index scans |
From executionStats, compare nReturned, totalKeysExamined and totalDocsExamined:
FETCH filter.SORT stage is present: the index order does not match the sort; revisit the ESR order.// Covered: filter, sort and projection all inside { status: 1, createdAt: -1 }
db.orders.find({ status: "open" }, { _id: 0, status: 1, createdAt: 1 })
.sort({ createdAt: -1 })
.explain("executionStats").executionStats.totalDocsExamined // 0The planner tries candidate plans briefly, keeps the winner in the plan cache and reuses it for queries with the same shape. Sometimes you need to intervene:
db.orders.find({ status: "open" }).hint({ status: 1, createdAt: -1 }) // force an index
db.orders.find({ status: "open" }).hint({ $natural: 1 }) // force a scan
db.orders.getPlanCache().clear() // after adding/dropping indexesTreat hint() as a diagnostic tool; a plan that only works with a hint usually means the index design should change.
Every mongod logs operations slower than slowms (100 ms by default) to its log file. The profiler stores the same information as documents in the capped system.profile collection of each database:
db.setProfilingLevel(1, { slowms: 50, sampleRate: 0.5 }) // level 1 = slow ops only
db.getProfilingStatus()
db.system.profile.find({ millis: { $gt: 50 } }, { op: 1, ns: 1, millis: 1, planSummary: 1, docsExamined: 1 })
.sort({ ts: -1 }).limit(10)planSummary is a one-line version of the plan (COLLSCAN or IXSCAN { status: 1 }), so a single query on system.profile lists every operation that scanned. Level 2 records everything and is only for short debugging sessions; set it back to 0 afterwards. For what is running right now, db.currentOp({ secs_running: { $gt: 5 } }) lists long operations and db.killOp(opid) stops one. Atlas exposes the same data in its Query Profiler and Performance Advisor.
explain("executionStats") runs the query, including full scans on large collections.`executionStats` shows `nReturned: 10`, `totalKeysExamined: 10`, `totalDocsExamined: 5000`. What is the most likely cause?
explain("executionStats") shows the winning plan tree and real counters; read stages from the innermost inputStage outwards.COLLSCAN and blocking SORT stages signal a missing or badly ordered index.nReturned, totalKeysExamined and totalDocsExamined; a covered query examines zero documents.hint() forces an index for diagnosis; clear the plan cache after changing indexes.slowms records slow operations in system.profile; planSummary reveals scans, and currentOp() shows what is running now.Next lesson: The Aggregation Pipeline — transform and summarise data in stages with $match, $group, $project and $sort.