Query Performance
Understanding Query Plans
Winning plan, rejected plans, and what executionStats adds on top of queryPlanner.
A plan is a tree of stages. You read it inside-out: the leaf does the scan, parents FETCH, SORT, LIMIT, PROJECTION. winningPlan is what ran. rejectedPlans are what lost the race — useful when the wrong index won.
- 01IXSCAN (city_1)
- 02FETCH (load documents)
- 03SORT (if the index did not provide order)
- 04LIMIT / PROJECTION
db.products.createIndex({ inStock: 1, price: 1 })
db.products.find(
{ inStock: true },
{ _id: 0, inStock: 1, price: 1 }
).sort({ price: 1 }).explain("executionStats")
// IXSCAN only — no FETCH. totalDocsExamined: 0A covered query never loads the document. _id is in every document and in the _id index — if you project _id (the default) the query is not covered unless _id is also in your compound index. { _id: 0 } is part of covering. Do not add five extra keys to cover a weekly report.
queryPlanner tells you which plan. executionStats tells you what it cost. allPlansExecution shows trial stats for the competitors — use it when two indexes look plausible and the wrong one wins. A SORT stage above FETCH means the index did not satisfy .sort() (ESR). LIMIT pushed into the index is why $sort+$limit is cheap.
Interview question
What is a covered query?
One answered entirely from an index: every field in the filter and the projection is in that index, and _id is excluded unless it is in the index too. explain shows IXSCAN without FETCH. Worth it on a hot path; not worth bloating an index for a rare report.
Practice
Filter, sort, project — the plan you want is IXSCAN, not SORT.