$planCacheStats lists cached query plans for a collection—which query shapes MongoDB remembers, which plan was chosen, and how costly that plan looked during evaluation.
Remember
$planCacheStats = "show cached plans for THIS collection"
db.orders.aggregate([
{ $planCacheStats: {} }
])
MUST be the FIRST stage
one output doc per cache entry
planCacheKey → identity of the cached plan
works → cost measured during plan trial
cachedPlan → chosen execution plan (often IXSCAN)
explain() → plan for ONE query now
$planCacheStats → ALL cached plans on the collection
pair with $indexStats for index usage
assert: $planCacheStats is stage 1
assert: run on the target collection
entries = plan cache for this collection
for each entry:
emit {
planCacheKey,
planCacheShapeHash,
works,
isActive,
cachedPlan,
createdFromQuery / query shape fields, …
}
// later $match / $project reshape the list
// empty cache → empty result (run queries first)
Reference
Cheat Sheet
Keep these facts handy during performance reviews.
Topic
Detail
Type
Diagnostic aggregation stage
Position
Must be first
Scope
One collection’s plan cache
Privilege
Often planCacheRead
Not allowed in
Transactions, $facet sub-pipelines
vs explain()
All cached plans vs one live plan
Empty result
Cache not warmed yet
Try it
Live Preview
Filter a sample plan-cache snapshot (teaching model—not a live mongod connection).
Sample entries show planCacheKey, stage type, and works.
Result
A1F3 IXSCAN works: 12 active
B2C4 IXSCAN works: 18 active
C9D0 COLLSCAN works: 120 inactive
Trace
Worked Walkthrough — Warm Cache, Then Inspect
Goal: run a query so MongoDB caches a plan, then list cache entries.
Calculation
1. Create index + run find({ status: "active" })
→ planner picks a plan and may cache it
2. db.orders.aggregate([
{ $planCacheStats: {} } ← first stage
])
3. Look for:
planCacheKey → identify the entry
works → lower often means cheaper trial
cachedPlan → IXSCAN vs COLLSCAN
4. Optional: $project / $sort for a short report
Code
MongoDB Examples
We seed a small orders collection in Example 1, warm the plan cache with real queries, then inspect cache metadata in the following examples.
Example 1 — Sample Data
Three orders, an index on status, then warm the plan cache with two different query shapes.
mongosh
db.orders.drop()
db.orders.insertMany([
{ status: "active", total: 40 },
{ status: "active", total: 15 },
{ status: "closed", total: 99 }
])
db.orders.createIndex({ status: 1 })
// Warm the plan cache (planner may store a plan per shape)
db.orders.find({ status: "active" }).toArray()
db.orders.find({ status: "closed" }).toArray()
Seed data. Three orders give the planner something to scan.
Add an index.createIndex({ status: 1 }) changes plan choices.
Warm the cache. Each distinct find() shape can create a cache entry.
Next examples.$planCacheStats lists those entries—not the order rows.
Example 2 — List Cache
Goal: after Example 1, list every cached plan for orders.
mongosh
db.orders.aggregate([
{ $planCacheStats: {} }
])
Output (sample fields)
{
planCacheKey: "...",
planCacheShapeHash: "...",
isActive: true,
works: 12,
cachedPlan: { /* stage tree */ }
}
// Exact field names vary slightly by MongoDB version
// Empty [] if Example 1 warm-up was skipped
How It Works
First stage. Empty {} must lead the pipeline on orders.
One doc per entry. Each row is a cached plan, not an order document.
Needs warm-up. Run Example 1 first or you may get an empty result.
Live metadata. Output reflects your server’s plan cache right now.
Example 3 — Clean Report
Goal: after Example 1, keep only the fields you need and sort by trial cost (works).
{
planCacheKey: "...",
works: 12,
stage: "IXSCAN"
}
// stage path may nest deeper depending on version/plan shape
How It Works
Match after. Filter cache docs with isActive: true.
Project stage. Surface whether the plan looks like IXSCAN.
Tune loop. Pair with explain() on the real query.
Clear when testing.getPlanCache().clear() resets entries.
Edge Cases & Pitfalls
Check these before relying on plan-cache reports.
order
Must be the first stage
Put $match / $project after it. Nothing may precede $planCacheStats.
empty
Empty cache ⇒ empty result
Run real queries first. A fresh collection often has no cache entries yet.
scope
Not for $facet or transactions
Run as a top-level first stage during diagnostics—not inside app transaction logic.
fields
Field names can vary by version
Older servers may show queryHash instead of planCacheShapeHash. Check one raw document first.
Analysis
Performance Notes
Approach
Cost
Best for
Default $planCacheStats
Light metadata read
Quick audits
$project + $limit
Smaller payloads
Dashboards
Polling every second
Extra admin load
Avoid unless needed
Pair with $indexStats
Two diagnostic passes
Index + plan reviews
Prefer slim projections. Use explain() when you need deep detail for one query shape.
Remember
Key Takeaways
Role: list cached query plans for one collection.
Rules: must be first; warm the cache before inspecting.
Focus:planCacheKey, works, cachedPlan, isActive.
Pair with:explain() and $indexStats for full tuning.
One line:$planCacheStats answers “which query plans has MongoDB cached for this collection?”
Frequently Asked Questions
$planCacheStats returns one document per cached query plan for the collection you run it against. Entries include planCacheKey, query shape info, cachedPlan, works (trial cost), and whether the entry is active.
It must be the first stage: db.orders.aggregate([{ $planCacheStats: {} }]). You can follow it with $match, $sort, or $project—but nothing may precede it.
planCacheShapeHash (queryHash on older versions) identifies the query shape. planCacheKey includes the shape plus available indexes, so the same shape with different indexes can produce different keys.
explain() shows the plan for one query you run now. $planCacheStats lists all plans MongoDB has cached for the collection—useful for auditing stored shapes and active plans.
With authorization enabled, you need planCacheRead on the collection. It is a diagnostic stage for DBAs and tuning—not for application-facing pipelines.
No. $planCacheStats is not allowed inside transactions or $facet sub-pipelines. Run it as a top-level first stage on the target collection.