What does explain() on an aggregation pipeline show that the pipeline text does not?
answer
- what you get back is not what you typed
- the array reflects the optimizer's rewrites
- one stage marks where the query layer stopped
- sharded output names who merges
- compare plans before and after a change
basics
~20 sExplain output shows the optimized pipeline — the stages array after reordering and coalescing — plus a leading $cursor stage holding the query plan for the part the query layer executed. On a sharded cluster it also shows how the pipeline was split between shards and the merging node.
solid answer
~50 sRun it as `db.coll.explain("executionStats").aggregate([...])`. Three things become visible that the pipeline source cannot tell you. First, the `stages` array is the pipeline **after** optimization, so you can see whether your `$match` was pushed ahead of a `$project` or `$sort`, whether adjacent stages were coalesced, and whether a `$limit` was folded into a `$sort`. Second, the head of that array is a `$cursor` stage containing `queryPlanner` and, with `executionStats`, the execution counters for the part that ran in the query layer — that stage is precisely the boundary between indexed collection access and in-memory pipeline processing. Third, on a sharded collection the output shows how the work was split: a shards section for the per-shard portion and a merger portion for what runs on the merging node. If the stages array looks exactly like what you typed, no rewriting happened and any reordering you wanted is yours to write.
code
javascript · 7 linesdb.orders.explain("executionStats").aggregate([
{ $sort: { createdAt: -1 } },
{ $match: { status: "paid" } },
{ $limit: 20 }
])
// read back: is $match now inside the leading $cursor stage,
// and did $limit get folded into the sort?go deeper
Know that you can ask the server to explain an aggregation, and that the answer tells you whether the pipeline read the collection through an index or scanned it. Running it and finding the leading stage is enough.
Explain that the returned stages array is the post-optimization pipeline, and that the leading $cursor stage is where query-layer execution ends and aggregation-layer processing begins.
Use it as evidence: capture plans before and after a change, show the filter moving into the cursor stage or the limit folding into the sort, and read sharded output to see which stages run once on the merging node.
Make plan evidence part of how expensive pipelines are reviewed and how regressions are caught, and set the expectation that performance claims come with before-and-after plans from production-shaped data.
## Asking for it Two equivalent forms exist: ```javascript db.orders.explain("executionStats").aggregate(pipeline) db.orders.aggregate(pipeline, { explain: true }) ``` The verbosity argument follows the usual ladder — plan-only, plan plus execution counters, or all considered plans. Plan-only tells you what the server intends to do; execution statistics tell you what it actually did, which is what you want when a pipeline is slow rather than merely suspicious. ## The stages array is the optimized pipeline The most under-used fact about aggregation explain output is that the `stages` array is not your pipeline — it is the pipeline the optimizer produced. Every rewrite is visible there: - a `$match` that has moved ahead of a `$project`, `$addFields` or `$sort`; - two `$match` stages merged into a single predicate; - consecutive `$limit` or `$skip` stages collapsed; - a `$limit` absorbed into the preceding `$sort`, so the sort retains only the top *n*. This converts "I think MongoDB pushes filters down" into an observation. If your `$match` is still sitting where you wrote it, the optimizer could not move it — usually because the predicate touches a field a preceding stage computed — and you now know it is your job to restructure the pipeline. ## The $cursor stage is the boundary When a pipeline starts by reading a collection, the leading element of `stages` is a `$cursor` stage. It contains a `queryPlanner` section — and, under `executionStats`, the execution counters — for the query the aggregation framework handed to the ordinary query layer. That stage is conceptually the seam of the whole system. Everything inside `$cursor` is normal query execution: a predicate, possibly a projection, an index scan or a collection scan. Everything after it is aggregation-layer work over a stream of documents, where no index can help. When someone asks "did my pipeline use the index?", the honest answer is "look inside `$cursor`" — and the corollary is that if the filter you care about is *not* inside `$cursor`, it never had a chance to. The counters inside that stage are read the same way as for any query, which is the ordinary query-plan skill; the pipeline-specific insight is which part of your pipeline made it in there at all. ## Sharded pipelines Against a sharded collection the shape changes. The output describes how the pipeline was divided: a portion executed on each shard, and a merging portion executed on the router or a designated shard, with a section naming which stages went where. This is how you discover that a stage you assumed ran in parallel is actually running once on the merging node over everything the shards sent back — a common cause of a pipeline that scales badly as shards are added. ## Other things worth noticing Recent server versions report an `explainVersion` field, because the newer execution engine produces a different plan representation; do not be alarmed when the plan tree looks unfamiliar between versions, and compare like with like when you keep before/after plans. ## What explain does not tell you It does not tell you whether the pipeline is *correct*, and it does not report business meaning. It also does not by itself tell you whether a blocking stage will spill on tomorrow's data volume — you infer that from how many documents reach the stage and how many distinct groups it builds. And a plan captured on a small development dataset can differ from production, because the choice among candidate plans depends on the data. ## Using it well The habit that pays: capture explain output before a change and after it, and compare the optimized `stages` array and the `$cursor` plan side by side. "It feels faster" is not evidence; "the leading `$match` now appears inside `$cursor` on an index scan, and the limit is folded into the sort" is.
- How do you confirm from explain output that a $match was pushed down?Read the `stages` array, which is the optimized pipeline. If the predicate now appears inside the leading `$cursor` stage's query — or ahead of the `$project`/`$sort` you wrote it after — the push happened. If it still sits where you typed it, it could not move, typically because it filters a field an earlier stage computed.
- Explain shows your $limit is not folded into the $sort. What would you look at?The stage sitting between them. The fold requires that nothing in between changes the number of documents, so an intervening `$match`, `$unwind` or `$group` blocks it. Move the `$limit` directly after the `$sort` if the semantics allow, and re-read the optimized stages to confirm the limit now appears as part of the sort.
- Why can a plan captured on your laptop mislead you about production?Plan choice depends on the data: which indexes exist, how many documents match, and what the server has learned from executing candidate plans. A small development dataset can make a collection scan look fine or make a different index win. Capture explain output against production-shaped data, and treat a local plan as a hypothesis rather than proof.
saying these in an interview costs you the question
- Assumes the stages array is the pipeline as written
- Cannot say which part of a pipeline the query layer executed
- Reads only timing and never the optimized stage order
- Ignores the split between shard and merger work on sharded collections
- Claims a plan from a tiny dev dataset proves production behaviour