The $indexStats stage reports how often each index is used on a collection—helping you spot unused indexes wasting disk and write overhead, and confirm that your query indexes are actually being hit.
01
Per-index stats
One doc each.
02
access.ops
Usage counter.
03
access.since
Tracking start.
04
Find unused
Zero ops filter.
05
First stage
Pipeline rule.
06
vs $collStats
Usage vs size.
Fundamentals
Definition and Usage
$indexStats is an aggregation stage that inspects every index on the current collection and returns usage metadata. Unlike stages that transform application data, it is a diagnostic tool for DBAs and developers tuning query performance.
💡
Beginner tip
access.ops tells you how many operations used an index since tracking started—not how big the index is. Pair $indexStats with $collStats storage stats when deciding whether to drop a low-usage index that still consumes significant disk.
Use $indexStats during performance audits, after adding new indexes, or before removing old ones. Follow with $sort to rank by usage and $match to isolate indexes with zero operations.
Foundation
📝 Syntax
$indexStats takes an empty document—no options to configure:
mongosh
{ $indexStats: {} }
Syntax Rules
First stage — $indexStats must be stage 1; no stage may precede it.
Empty object — pass {} only; other fields are not supported.
One doc per index — output includes the default _id_ index plus all user-created indexes.
Sample products collection with several indexes—run queries to generate usage, inspect stats, rank by ops, find unused indexes, and build a clean monitoring report.
📚 Getting Started
Seed data, create indexes, and run sample queries.
Example 1 — Products collection with multiple indexes
Four user indexes plus the default _id_ index. Running queries before $indexStats increases access.ops on indexes those queries use—making the examples realistic.
5 indexes → 5 output documents
Each includes name, key, access.ops
_id_ always present (used internally)
How It Works
$indexStats scans index metadata—not your product documents. The stage must be first; it returns immediately with current counters from the server.
📈 Practical Patterns
Rank usage, spot unused indexes, and build audit reports.
Example 3 — Sort indexes by usage (most used first)
mongosh
db.products.aggregate([
{ $indexStats: {} },
{ $sort: { "access.ops": -1 } },
{
$project: {
indexName: "$name",
keyPattern: "$key",
ops: "$access.ops",
trackingSince: "$access.since",
_id: 0
}
}
])
// Highest access.ops → most frequently used index
// Useful after load tests to confirm expected indexes are hit
📤 Ranked by ops:
_id_ often high (every lookup uses it)
sku_1, category_1 after sample queries
inStock compound may rank lower
How It Works
Sorting by access.ops highlights your workhorse indexes. Cross-check with db.collection.explain() on slow queries to verify the planner picks the index you expect.
Example 4 — Find potentially unused indexes
mongosh
db.products.aggregate([
{ $indexStats: {} },
{
$match: {
"access.ops": 0,
name: { $ne: "_id_" }
}
},
{
$project: {
indexName: "$name",
keyPattern: "$key",
ops: "$access.ops",
since: "$access.since",
_id: 0
}
}
])
// Excludes _id_ (always needed)
// Zero ops → candidate for review (not automatic drop!)
// Check replica set members, staging vs prod, and access.since date
📤 Unused candidates:
inStock_1_category_1 may show ops: 0
Verify with $collStats indexSizes before dropping
Never drop without staging + backup plan
How It Works
Zero access.ops suggests an index has not been used since tracking started—but confirm across all app traffic patterns. A compound index may appear unused if queries only hit a prefix covered by another index.
Example 5 — Combined audit with $collStats index sizes
mongosh
// Step 1: index usage from $indexStats
const usage = db.products.aggregate([
{ $indexStats: {} },
{
$project: {
indexName: "$name",
ops: { $toLong: "$access.ops" },
keyPattern: "$key",
_id: 0
}
}
]).toArray()
// Step 2: index sizes from $collStats (separate pipeline)
const sizes = db.products.aggregate([
{ $collStats: { storageStats: {} } },
{ $project: { indexSizes: "$storageStats.indexSizes", _id: 0 } }
]).toArray()
// Merge in application code for a full audit:
// indexName | ops | sizeBytes | keyPattern
// Compare usage vs disk cost before dropIndex()
// vs explain("executionStats"):
// db.products.find({ sku: "A100" }).explain("executionStats")
// → winningPlan.inputStage.indexName confirms index used
📤 Full audit workflow:
$indexStats → which indexes are used
$collStats → how much disk each uses
explain() → verify specific queries
How It Works
Production index reviews combine usage ($indexStats), cost ($collStats index sizes), and query plans (explain). Drop indexes only when usage is consistently zero across a full traffic cycle and no unique constraint is needed.
Compare
📋 $indexStats vs related diagnostic tools
Tool / stage
What it shows
Best for
$indexStats
Per-index usage (access.ops)
Finding unused or underused indexes
$collStats
Storage, count, index sizes, latency
Disk footprint and collection health
explain()
Query plan for one specific query
Verifying a slow query uses the right index
$currentOp
Active operations on the server
Live blocking/slow query investigation
db.collection.getIndexes()
Index definitions (no usage stats)
Listing what indexes exist
🧠 How $indexStats Works
1
Stage runs first
MongoDB reads index catalog metadata for the target collection—no input documents required from prior stages.
Catalog
2
Counters read
For each index, the server reports access.ops and access.since from internal tracking since startup or index creation.
Metrics
3
One doc per index
Each index becomes an output document with name, key, spec, and host (plus shard on sharded clusters).
Emit
=
📈
Usage report
Downstream $sort and $match stages shape the data for dashboards or DBA review.
Important
📝 Notes
$indexStats must be the first pipeline stage.
Pass an empty object only: { $indexStats: {} }.
access.ops resets when the mongod process restarts or when the index is rebuilt.
On replica sets, run on each member—usage patterns can differ by read preference routing.
Never drop the _id_ index—it is required on every collection.
$indexStats is your first stop for index hygiene: run it on each collection, sort by access.ops, and investigate zero-usage indexes before dropping them.
Combine with $collStats for disk cost and explain() for individual query plans. Next: $limit to cap pipeline output rows.
Run $indexStats after representative load tests or during low-traffic maintenance windows
Check access.since—a brand-new index with zero ops is not necessarily unused
Pair usage stats with $collStats index sizes before dropIndex()
Exclude _id_ when hunting unused indexes
Verify on all replica set members in production-like environments
❌ Don’t
Drop indexes based on a single zero-ops reading without broader validation
Put stages before $indexStats in the pipeline
Assume access.ops includes operations from other collections
Confuse index size ($collStats) with index usage ($indexStats)
Expect $indexStats to work inside $facet sub-pipelines
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $indexStats
Use these points whenever you tune indexes or review query performance.
5
Core concepts
📈01
access.ops
Usage count.
Metric
📚02
Stage 1 only
Pipeline rule.
Syntax
🔍03
One per index
Flat output.
Shape
🗑04
Zero ops
Review, don’t rush.
Audit
📊05
+ $collStats
Usage + size.
Combo
❓ Frequently Asked Questions
$indexStats returns usage statistics for every index on the collection you run it against—one output document per index. Each result includes the index name, key pattern, host, and access.ops (how many times the index was used since tracking started).
$indexStats must be the first stage: db.products.aggregate([{ $indexStats: {} }]). If any stage precedes it, MongoDB returns an error. You can add $match, $sort, or $project after it to filter or reshape results.
access.ops is a counter of index operations—reads and writes that used this index—since the mongod process started or since the index was created (whichever is more recent). access.since shows when counting began.
Often yes, but not always. A new index, a recently restarted server, or an index only used on another replica set member may show low or zero ops. Confirm with query patterns, explain plans, and production traffic before dropping an index.
$collStats reports collection storage, document count, and index sizes. $indexStats reports per-index usage (access.ops)—which indexes queries actually hit. Use both: $collStats for disk footprint, $indexStats for optimization.
No. Like $collStats, $geoNear, and $currentOp, $indexStats cannot appear inside $facet sub-pipelines. Run it as a top-level stage on the target collection.
Did you know?
$indexStats was added in MongoDB 3.2. On sharded clusters, each shard reports its own stats—run the aggregation on mongos to collect per-shard results. MongoDB Atlas Performance Advisor also surfaces index suggestions based on similar usage analysis. See the official $indexStats docs.