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 tototalKeysExaminedinexplain("executionStats").metrics.queryExecutor.scannedObjects: documents examined. This maps tototalDocsExamined.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
| Cause | What it looks like | First thing to check |
|---|---|---|
| Missing index on a queried field | Slow 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 index | explain() 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 semantics | A $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 index | Queries 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 streams | Background 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]Confirm the ratio from deltas. Do not divide absolute cumulative values. Sample
metrics.queryExecutorandmetrics.documentat the start and end of an interval, then divide the deltas. A one-minute sample is usually enough unless query volume is very low.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.
Correlate with the slow query log. Look for
COLLSCAN, highdocsExamined, or highkeysExaminedrelative tonreturned. The canonical signature of a missing index isplanSummary: COLLSCAN keysExamined:0 docsExamined:10000 nreturned:4.Run
explain("executionStats")on suspicious queries. ComparetotalKeysExamined,totalDocsExamined, andnReturned. AtotalDocsExamined / nReturnedratio above 100:1 for an OLTP query is almost always worth fixing.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.
Rule out Atlas Search background cursors if applicable. On Atlas with Atlas Search enabled,
mongotchange streams can inflate the server-wide ratio. Filter those workloads out of your investigation before rebuilding indexes.Check aggregation pipelines separately. The ratio applies to the
$matchstage 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
| Signal | Why it matters | Warning sign |
|---|---|---|
metrics.queryExecutor.scannedObjects / metrics.document.returned | Measures wasted document fetches per result. | Sustained >100:1, or Atlas alert at >1000:1. |
metrics.queryExecutor.scanned / metrics.document.returned | Measures wasted index traversal per result. | Rising above 10:1 for OLTP workloads. |
| Slow query rate | Direct evidence of inefficient execution. | Sudden increase versus baseline. |
currentOp max running time | Catches scans before they hit the slow threshold. | Unexpected operations >60 seconds. |
| WiredTiger cache fill ratio | Wasted work loads more pages into cache. | Fill rising together with scanned-objects rate. |
| WiredTiger read ticket availability | Inefficient 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
COLLSCANpatterns are easiest to fix before the collection grows. - Lock index changes in change control. Accidental
dropIndexevents 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
totalDocsExaminedto zero.
How Netdata helps
- Netdata collects
metrics.queryExecutor.scanned,scannedObjects, andmetrics.document.returnedand derives the scanned-to-returned ratio automatically. - You can correlate the ratio with WiredTiger cache pressure, ticket utilization, and
opLatencieson 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.
Related guides
- How MongoDB actually works in production: a mental model for operators
- MongoDB pages evicted by application threads: when eviction becomes user latency
- MongoDB WiredTiger cache dirty ratio high: the leading indicator nobody watches
- MongoDB WiredTiger cache pressure cascade: eviction stalls and latency spikes
- MongoDB cache too small: sizing the WiredTiger cache for your working set
- MongoDB checkpoint duration climbing: diagnosing slow WiredTiger checkpoints
- MongoDB checkpoint stall write freeze: when all writes stop with no error
- MongoDB connection churn: high totalCreated rate and thread creation overhead
- MongoDB connection refused at maxIncomingConnections: hitting the connection ceiling
- MongoDB connection storm spiral: reconnection floods after an election or deploy
- MongoDB exceeded memory limit for $group — aggregation spills and allowDiskUse
- MongoDB flow control throttling writes: when the primary slows itself down







