MongoDB $indexStats Stage

Beginner
⏱️ 10 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

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.

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.

📝 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.
  • Key output fieldsname, key, access.ops, access.since, spec, host.
  • Post-processing — chain $match, $sort, $project after $indexStats.
  • Not in $facet — cannot nest inside $facet sub-pipelines.
mongosh
db.products.aggregate([
  { $indexStats: {} }
])

⚡ Quick Reference

QuestionAnswer
Stage positionMust be first in the pipeline
OptionsNone—use { $indexStats: {} }
Usage metricaccess.ops (NumberLong counter)
Tracking sinceaccess.since (ISODate)
Index key patternkey field (e.g. { sku: 1 })
Sharded clusterOne result per index per shard (includes shard field)
Basic run
db.products.aggregate([
  { $indexStats: {} }
])

All indexes

Most used
{ $sort: {
  "access.ops": -1
}}

Rank by ops

Unused
{ $match: {
  "access.ops": 0
}}

Zero ops

Clean view
{ $project: {
  name: 1, key: 1,
  ops: "$access.ops"
}}

Trim fields

🧰 Output Fields

Each document returned by $indexStats describes one index:

name Output

Index name—_id_ for the default index, or the name you passed to createIndex().

name: "sku_1"
name: "_id_"
key Output

Index key pattern showing field names and sort direction (1 ascending, -1 descending).

key: { sku: 1 }
key: { category: 1, price: -1 }
access.ops Output

Number of index operations since access.since. Higher values mean more query/write usage.

access: {
  ops: NumberLong("842"),
  since: ISODate("...")
}
spec Output

Full index specification—includes v, key, and options like unique or sparse.

spec: {
  v: 2,
  key: { sku: 1 },
  name: "sku_1",
  unique: true
}

Examples Gallery

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

mongosh
db.products.drop()
db.products.insertMany([
  { sku: "A100", name: "Wireless Mouse",  category: "Electronics", price: 29,  inStock: true },
  { sku: "B200", name: "USB Keyboard",    category: "Electronics", price: 49,  inStock: true },
  { sku: "C300", name: "Office Chair",    category: "Furniture",   price: 199, inStock: false },
  { sku: "D400", name: "Desk Lamp",       category: "Furniture",   price: 35,  inStock: true },
  { sku: "E500", name: "Monitor Stand",   category: "Electronics", price: 59,  inStock: true }
])

db.products.createIndex({ sku: 1 }, { unique: true })
db.products.createIndex({ category: 1 })
db.products.createIndex({ price: -1 })
db.products.createIndex({ inStock: 1, category: 1 })  // rarely queried

// Generate index usage with typical queries
db.products.find({ sku: "A100" }).toArray()
db.products.find({ category: "Electronics" }).sort({ price: -1 }).toArray()
db.products.find({ price: { $gte: 50 } }).toArray()

How It Works

Four user indexes plus the default _id_ index. Running queries before $indexStats increases access.ops on indexes those queries use—making the examples realistic.

Example 2 — Basic index statistics

mongosh
db.products.aggregate([
  { $indexStats: {} }
])

/* Sample output (one doc per index):
{
  name: "sku_1",
  key: { sku: 1 },
  host: "hostname:27017",
  access: { ops: NumberLong("1"), since: ISODate("...") },
  spec: { v: 2, key: { sku: 1 }, name: "sku_1", unique: true }
}
{ name: "category_1", key: { category: 1 }, access: { ops: ... }, ... }
{ name: "price_-1", ... }
{ name: "inStock_1_category_1", ... }
{ name: "_id_", key: { _id: 1 }, ... }
*/

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

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

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

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.

🧠 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.

📝 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.
  • Previous topic: $group. Next: $limit.

Conclusion

$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.

💡 Best Practices

✅ Do

  • 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

Key Takeaways

Knowledge Unlocked

Five things to remember about $indexStats

Use these points whenever you tune indexes or review query performance.

5
Core concepts
📚 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.

Continue the Stages Series

Audit indexes with $indexStats, then shape results with $limit.

Next: $limit →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful