MongoDB scanned objects ratio high: documents examined vs returned and wasted work

A high scanned-objects-to-returned-documents ratio means MongoDB examines far more documents than it returns. The alert usually fires as an Atlas query targeting event or a chart showing metrics.queryExecutor.scannedObjects climbing faster than metrics.document.returned. A ratio near 1:1 is efficient. A ratio above 1000:1 means the database examines roughly a thousand documents for every one it returns. Wasted work consumes read tickets, pushes unnecessary data through the WiredTiger cache, and often precedes latency spikes or ticket exhaustion.

What this means

The ratio uses three cumulative counters in db.serverStatus():

  • metrics.queryExecutor.scanned: index entries examined. This maps to totalKeysExamined in explain("executionStats").
  • metrics.queryExecutor.scannedObjects: documents examined. This maps to totalDocsExamined.
  • metrics.document.returned: documents returned to clients.

Because these counters never reset, compute the ratio from deltas over a sample interval. Atlas reports this as Scanned Objects / Returned and defaults its alert threshold to 1000:1. A covered query satisfied entirely from an index produces totalDocsExamined: 0, contributing zero to scannedObjects while still adding to returned. A collection scan produces keysExamined: 0 and docsExamined equal to the collection size. The ratio is server-wide, so a small number of bad queries can dominate the metric even when most of the workload is healthy.

The ratio measures efficiency, not absolute speed. A query can be fast on a small collection and still have a terrible ratio. As the collection grows, that same query becomes a latency and resource crisis.

Common causes

CauseWhat it looks likeFirst thing to check
Missing index on a queried fieldSlow query log shows planSummary: COLLSCAN with keysExamined:0 and docsExamined near the collection size.Compare query predicates to db.collection.getIndexes().
Unselective predicate on an existing indexexplain() shows IXSCAN but totalKeysExamined is hundreds or thousands of times larger than totalDocsExamined or nReturned.Look for $ne, $nin, broad regexes, or wide range scans.
Aggregation stage semanticsA $match followed by $group can show docsExamined equal to nReturned even when the final output is a single document.Check the full pipeline explain() rather than judging by the server-wide ratio alone.
Index regression or dropped indexQueries that were previously efficient suddenly show COLLSCAN or switch to a less selective index.Compare current indexes to expected ones; check for recent migrations or failed builds.
Atlas Search / mongot change streamsBackground cursors that keep Atlas Search indexes current inflate the ratio without a clear offending query in the slow log.Look for long-running change stream cursors in currentOp.

Quick checks

These checks are read-only and safe to run during an incident.

# Check current cumulative counters
mongosh --quiet --eval 'db.serverStatus().metrics.queryExecutor'
# Check documents returned counter
mongosh --quiet --eval 'db.serverStatus().metrics.document'
# Compute the ratio over a 60-second window
# Warning: this blocks the shell for the full sample interval.
mongosh --quiet --eval '
var a=db.serverStatus().metrics.queryExecutor;
var d1=db.serverStatus().metrics.document;
sleep(60000);
var b=db.serverStatus().metrics.queryExecutor;
var d2=db.serverStatus().metrics.document;
var so=b.scannedObjects-a.scannedObjects;
var ret=d2.returned-d1.returned;
print("scannedObjects_delta: " + so);
print("returned_delta: " + ret);
print("ratio: " + (ret > 0 ? (so/ret).toFixed(1) : "N/A"));
'
# Find recent slow queries with plan details
# Only works on self-hosted hosts with local log access.
grep "Slow query" /var/log/mongodb/mongod.log | tail -20
// Find operations running longer than 10 seconds
db.currentOp({ "active": true, "secs_running": { "$gt": 10 } }).inprog.forEach(function(op) {
  print(op.opid + " | " + op.secs_running + "s | " + op.ns + " | " + JSON.stringify(op.command).substring(0, 120));
});
// Per-index usage since process start
db.getCollectionNames().forEach(function(coll) {
  print("=== " + coll + " ===");
  db[coll].aggregate([{ $indexStats: {} }]).forEach(function(idx) {
    print("  " + idx.name + ": " + idx.accesses.ops + " ops");
  });
});

How to diagnose it

flowchart TD
    A[Rising scanned / returned ratio] --> B[Compute deltas from serverStatus]
    B --> C[Check slow query log for COLLSCAN]
    C --> D{COLLSCAN or high examined count?}
    D -->|Yes| E[Verify indexes vs query shape]
    D -->|No| F[Check currentOp for mongot cursors]
    E --> G[Build selective index or rewrite predicate]
    F --> H[Filter Atlas Search background work]
  1. Confirm the ratio from deltas. Do not divide absolute cumulative values. Sample metrics.queryExecutor and metrics.document at the start and end of an interval, then divide the deltas. A one-minute sample is usually enough unless query volume is very low.

  2. Determine whether it is sustained. A brief spike during a bulk import or an ad-hoc analytics query may self-resolve. A sustained climb over multiple minutes indicates an inefficient query path that is being exercised repeatedly.

  3. Correlate with the slow query log. Look for COLLSCAN, high docsExamined, or high keysExamined relative to nreturned. The canonical signature of a missing index is planSummary: COLLSCAN keysExamined:0 docsExamined:10000 nreturned:4.

  4. Run explain("executionStats") on suspicious queries. Compare totalKeysExamined, totalDocsExamined, and nReturned. A totalDocsExamined / nReturned ratio above 100:1 for an OLTP query is almost always worth fixing.

  5. Check for index regressions. Compare the current index list against your expected schema. Look for recently dropped indexes, failed background builds, or plan cache changes that pushed a query from a selective index to a collection scan.

  6. Rule out Atlas Search background cursors if applicable. On Atlas with Atlas Search enabled, mongot change streams can inflate the server-wide ratio. Filter those workloads out of your investigation before rebuilding indexes.

  7. Check aggregation pipelines separately. The ratio applies to the $match stage as seen by the query executor, not the final document count after $group. A pipeline that scans a million documents to return one aggregated result will look expensive by this metric even if it is the correct design.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
metrics.queryExecutor.scannedObjects / metrics.document.returnedMeasures wasted document fetches per result.Sustained >100:1, or Atlas alert at >1000:1.
metrics.queryExecutor.scanned / metrics.document.returnedMeasures wasted index traversal per result.Rising above 10:1 for OLTP workloads.
Slow query rateDirect evidence of inefficient execution.Sudden increase versus baseline.
currentOp max running timeCatches scans before they hit the slow threshold.Unexpected operations >60 seconds.
WiredTiger cache fill ratioWasted work loads more pages into cache.Fill rising together with scanned-objects rate.
WiredTiger read ticket availabilityInefficient queries hold tickets longer.Available tickets dropping during query spikes.

Fixes

Missing or wrong index

If explain() shows COLLSCAN, add an index that matches the query predicates in selective order. Use a background build in production to avoid blocking writes.

db.collection.createIndex({ tenant_id: 1, created_at: -1 }, { background: true })

Tradeoffs: every new index adds write amplification and consumes cache space. Do not create indexes for one-off queries without reviewing the write cost.

Unselective predicates

If the query uses $ne, $nin, leading wildcards, or very broad ranges, an existing index may still scan many keys. Rewrite the query to use equality or bounded range predicates where possible. If the access pattern is fundamentally unselective, consider whether the database is the right place for that computation, or if the result set should be cached upstream.

Index regression

If an index was dropped or a plan changed, recreate the missing index or force a plan re-evaluation.

Atlas Search background cursors

When Atlas Search mongot change streams drive the alert, the ratio is a side effect of keeping search indexes current. Coordinate with the search team to tune sync behavior, but do not add unrelated MongoDB indexes solely to suppress the metric.

Prevention

  • Monitor the ratio continuously. Do not wait for an Atlas alert. A baseline of 5:1 that drifts to 50:1 is an early signal of a new inefficient query.
  • Review slow query logs weekly. New COLLSCAN patterns are easiest to fix before the collection grows.
  • Lock index changes in change control. Accidental dropIndex events are a common cause of silent regression.
  • Test query plans before deploy. Run explain("executionStats") on representative queries in staging with production-like data volumes.
  • Design for covered queries where appropriate. If a query only needs fields that are already in a supporting index, avoiding document fetches drives totalDocsExamined to zero.

How Netdata helps

  • Netdata collects metrics.queryExecutor.scanned, scannedObjects, and metrics.document.returned and derives the scanned-to-returned ratio automatically.
  • You can correlate the ratio with WiredTiger cache pressure, ticket utilization, and opLatencies on the same time axis to see when wasted work turns into system-wide saturation.
  • Alerts can be set on the ratio itself, or on the slow query rate, to catch regressions before they hit Atlas threshold levels.
  • Per-second resolution helps distinguish a brief ad-hoc query spike from a sustained inefficient query pattern.