The $planCacheStats stage reveals cached query plans for a collection—what shapes MongoDB remembers, which indexes were chosen, and how costly each plan was during evaluation. DBAs use it to troubleshoot slow queries and verify the planner is caching sensible plans.
01
Cache entries
One per plan.
02
planCacheKey
Lookup hash.
03
works
Plan cost.
04
cachedPlan
Chosen stages.
05
First stage
Pipeline rule.
06
vs explain()
All vs one.
Fundamentals
Definition and Usage
When MongoDB runs a query, the query planner picks an execution plan—often an index scan (IXSCAN) or collection scan (COLLSCAN). Successful plans are stored in the plan cache so repeat queries skip re-planning.
💡
Beginner tip
Think of the plan cache as MongoDB’s memory of “how we ran this kind of query before”: explain() → plan for one query you just ran. $planCacheStats → list of all cached plans on a collection. Pair with $indexStats to see which indexes exist and which plans use them.
Run $planCacheStats during performance reviews or after deploying new indexes. It is a diagnostic stage—not for application data pipelines.
Foundation
📝 Syntax
$planCacheStats accepts an empty document or an optional sharded-cluster flag:
MongoDB only caches plans after queries execute. Run representative find() patterns first—then inspect the cache to see which shapes and indexes the planner stored.
Example 2 — List all plan cache entries
mongosh
db.orders.aggregate([
{ $planCacheStats: {} },
{
$project: {
planCacheKey: 1,
planCacheShapeHash: 1,
queryHash: 1,
isActive: 1,
works: 1,
timeOfCreation: 1,
host: 1,
_id: 0
}
}
])
// One document per cached plan
// planCacheShapeHash groups same query shapes
// Different planCacheKey → different index choices
// Output varies by MongoDB version and engine (classic vs SBE)
The bare minimum is { $planCacheStats: {} }. Add $project to hide verbose cachedPlan details until you need them—keeps output readable for beginners.
Example 3 — Filter by planCacheKey
mongosh
// Step 1: note a planCacheKey from the list-all query
// (yours will differ — copy from your output)
db.orders.aggregate([
{ $planCacheStats: {} },
{ $match: { planCacheKey: "B1435201" } },
{
$project: {
planCacheKey: 1,
planCacheShapeHash: 1,
works: 1,
isActive: 1,
cachedPlan: 1,
estimatedSizeBytes: 1,
_id: 0
}
}
])
// Returns one cache entry matching that key
// Inspect cachedPlan for IXSCAN vs COLLSCAN stages
// Same shape + different indexes → different planCacheKey
📤 Filter pattern:
$planCacheStats MUST be first
$match on planCacheKey second
Replace "B1435201" with your key
How It Works
After listing all entries, drill into one plan by copying its planCacheKey from the first run. This is the official MongoDB pattern for inspecting a specific cached plan in detail.
📈 Practical Patterns
Find expensive plans and cross-check with explain().
Example 4 — Sort by works (find costly cached plans)
mongosh
db.orders.aggregate([
{ $planCacheStats: {} },
{ $match: { isActive: true } },
{ $sort: { works: -1 } },
{
$project: {
planCacheKey: 1,
planCacheShapeHash: 1,
works: 1,
indexFilterSet: 1,
estimatedSizeBytes: 1,
_id: 0
}
},
{ $limit: 5 }
])
// Highest works first → plans that cost more during trial
// indexFilterSet: true → index filter applied to this shape
// Investigate high-works + COLLSCAN combinations
📤 Tuning workflow:
Sort works descending
Check cachedPlan for COLLSCAN
Add or fix indexes
Re-run queries → cache refreshes
How It Works
works reflects planner trial cost—not live query latency—but sorting by it helps prioritize which cached shapes deserve index review. Filter isActive: true to focus on plans the planner currently uses.
Example 5 — Cross-check with explain() for one query
mongosh
// Single-query plan (explain)
db.orders.find(
{ item: "abc", price: { $gte: Decimal128("10") } }
).explain("executionStats")
// Look at:
// queryPlanner.planCacheKey
// queryPlanner.planCacheShapeHash
// executionStats.executionStages.stage → IXSCAN or COLLSCAN
// All cached plans ($planCacheStats)
db.orders.aggregate([
{ $planCacheStats: {} },
{
$match: {
planCacheShapeHash: "478AD696" // from explain output
}
},
{
$project: {
planCacheKey: 1,
works: 1,
isActive: 1,
_id: 0
}
}
])
// explain() → one query, live stats
// $planCacheStats → every cached shape on the collection
📤 Two tools together:
explain() for debugging one slow query
$planCacheStats for collection-wide audit
Match hashes between both outputs
How It Works
Use explain() when a specific query is slow; use $planCacheStats when you want a bird’s-eye view of everything cached. The planCacheKey and shape hash in explain output link directly to cache entries.
Compare
📋 $planCacheStats vs related tuning tools
Tool / stage
What it shows
Best for
$planCacheStats
All cached query plans on a collection
Plan cache audit, DBA reviews
explain()
Plan + execution stats for one query
Debugging a specific slow query
$indexStats
Per-index usage (access.ops)
Finding unused indexes
$collStats
Collection size, storage, index sizes
Disk footprint analysis
db.collection.planCache()
Legacy plan cache API (deprecated path)
Prefer $planCacheStats in aggregation
🧠 How $planCacheStats Works
1
Queries run first
Application find(), aggregate(), and update() queries cause the planner to evaluate and cache winning plans.
Warm
2
Stage reads cache
$planCacheStats as stage 1 reads the in-memory plan cache for the target collection on the selected host.
Read
3
One doc per entry
Each cache entry becomes a pipeline document with keys, works, cachedPlan, and metadata like host and isActive.
Emit
=
📈
Tuning insights
Downstream $match and $sort help DBAs focus on expensive or unexpected cached plans.
Important
📝 Notes
$planCacheStats must be the first pipeline stage.
Cache contents depend on which mongod member you connect to—replica set members may differ.
MongoDB 8.0+ uses planCacheShapeHash; older versions expose queryHash for the same concept.
Plan cache entries can become inactive (isActive: false) when indexes change or the cache evicts stale plans.
Not allowed in transactions or $facet sub-pipelines.
$planCacheStats opens MongoDB’s plan cache for inspection—showing which query shapes are remembered, which indexes were chosen, and how costly each cached plan was to evaluate.
Use it alongside explain() and $indexStats for a complete tuning picture. Next: $project to shape documents in everyday pipelines.
Run representative queries first so the cache reflects real workload shapes
$project key fields first; expand cachedPlan only when debugging
Sort by works to prioritize expensive cached plans
Cross-check suspicious entries with explain("executionStats")
Run on each replica set member during performance investigations
❌ Don’t
Put stages before $planCacheStats in the pipeline
Use $planCacheStats in application-facing API pipelines
Assume an empty cache means queries are fast—run queries to populate it first
Confuse works with live query latency—they measure different things
Expect identical cache contents across all replica set members
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $planCacheStats
Use these points whenever you audit query plans on a collection.
5
Core concepts
📚01
Plan cache
Stored plans.
Basics
🔑02
planCacheKey
Filter target.
ID
⚙03
works
Trial cost.
Metric
📈04
Stage 1
First only.
Rule
🔍05
+ explain()
Drill down.
Workflow
❓ Frequently Asked Questions
$planCacheStats returns information about cached query plans for the collection you run it against—one output document per plan cache entry. Each entry shows the query shape, chosen plan (cachedPlan), planCacheKey, works (plan cost during trial), and whether the entry is active.
$planCacheStats must be the first stage: db.orders.aggregate([{ $planCacheStats: {} }]). You can follow it with $match, $sort, or $project to filter or reshape results—but nothing may precede it.
planCacheShapeHash (queryHash in older versions) identifies the query shape—the filter pattern without considering indexes. planCacheKey includes both the shape and available indexes, so the same shape with different indexes can produce different planCacheKey values.
explain() shows the plan for one specific query you run right now. $planCacheStats lists all plans MongoDB has cached for the collection—useful for auditing what query shapes are stored and which cached plans are active.
On systems with authorization enabled, the user needs the planCacheRead privilege on the collection. It is a diagnostic stage intended for DBAs and performance 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 during maintenance or troubleshooting windows.
Did you know?
The same query shape can produce multiple cache entries with different planCacheKey values when indexes change—because the key includes both shape and index availability. After adding an index, old inactive entries may linger until evicted. See the official $planCacheStats docs.