Explain plans
Explain asks the server how it would run the current query, and how much work that took. It is where you find out that the query is fine and the collection has no index.
What it does and does not touch
Explain plans and measures the query. It does not return rows, and it does not replace what is in your results grid — it is a separate answer about the same query. That is why it never asks for confirmation the way re-running a query does, and why pending grid edits are safe across it.
It explains the selection if you have one, and the whole statement otherwise — the same rule as running.
TableCore asks for executionStats, not queryPlanner. Without real counters
you cannot tell a cheap index hit from an expensive one.
What the summary shows
| Reading | Meaning |
|---|---|
| Returned | Rows the query produced |
| Documents examined | Documents the server had to look at |
| Index keys examined | Index entries the server had to look at |
| Server time | How long the server spent on it |
| n documents examined for every row returned | The ratio that tells you whether an index is doing its job |
The ratio is the number to read first. One document examined per row returned is an index doing exactly what it should. A thousand is a scan wearing a filter.
It is undefined when nothing was returned — dividing by zero would print infinity, which is not information.
Collection scans, including the ones that hide
Collection scan — no index used is carried explicitly rather than left for you to spot in a list of stage names, and it covers the whole run rather than just the first read. It detects:
- a
COLLSCANnested under another stage, - a
$lookupfor which the server counted collection scans, - a slot-based
EQ_LOOKUPwhose strategy is not an indexed loop join.
That last one matters most. A $lookup that reads the foreign collection once
per input document is the most expensive thing an aggregation can do, and it
never appears as a COLLSCAN in the outer plan. Without this check the plan
looks clean.
What to do about a collection scan
Create an index on the field being filtered, sorted or joined on, and explain again. Indexes covers creating them and reading their usage statistics — that page is what this one is for.
Reading the two shapes
A find plan is a tree, and a pipeline is a sequence, because they are
read differently.
- Pipeline: a list of boxes in execution order — position, stage name and
that stage's own counters. Expanding one shows the rest of its counters, and
the read plan nested under the
$cursorstage where the server puts it. The stage that reads the whole collection is expanded by default: it is the one finding you should not have to hunt for. find: the plan tree, as the server nests it.
Returned for a pipeline comes from the last stage; the $cursor counter
only says how many documents the read handed onward. Examined counts are summed
across pipeline stages, since those are separate reads — but never across nodes
of a single find plan, where they measure one read at several levels and
adding them would count it twice.
The Raw tab always holds the server's whole response, whatever the summary managed to make of it. A plan that cannot be summarised is still worth showing.
Which queries can be explained
Anything the engine runs as one command, including the aggregation shapes —
JOIN, GROUP BY, HAVING, DISTINCT, computed columns. Those are not
rebuilt for the explain: the engine is asked for the command it would actually
send, and that command is explained.
A query that runs as several commands is named rather than planned:
This query shape cannot be explained. The engine could not establish a single command for this run, so any plan shown here would describe a different query.
For the shapes with a known name — a set operation, a subquery read while the plan is being built — the dialog leads with the disclosure instead: the reason, then the numbered steps in execution order, each marked as happening on the server or in TableCore. Describing one of several commands as the whole answer is the lie by omission this feature exists to prevent.
A global aggregate is a special case and not a refusal: the command is real, it is just not the last thing that happens. You get the plan and the steps.
Why you can trust that the plan is the query
Until the engine could capture commands, explain guaranteed correctness by refusing everything it could not rebuild — it could not describe the wrong command because it described none.
Now that it explains a captured command, the guarantee is a test instead: an
opt-in suite against a real MongoDB runs each shape and explains it, and reads
the command to compare from the server's own echo — explain repeats the
command it received. So the other side of the comparison is what the server got,
not what the engine believes it sent.
Server versions
The reader is defensive on purpose: the response shape differs between server
versions and between find and aggregate. Newer servers wrap the tree in
winningPlan.queryPlan; an aggregation reports its planner under
stages[0].$cursor. A missing field yields nothing rather than an exception.
One version difference is worth knowing about. When the server used its
slot-based execution engine, executionStages is a physical plan whose stage
names (scan, hash_lookup, nlj) share no vocabulary with the logical plan
and contain neither COLLSCAN nor EQ_LOOKUP. TableCore takes the shape
from the logical tree in that case and the numbers from the totals beside it.
Measured on MongoDB 8.0.6, an ordinary SELECT … GROUP BY goes down that path
by default, and a full collection scan was being reported as clean — the worst
way this feature can be wrong.