Performance & Query Optimization
MongoDB performance problems typically fall into three categories: missing indexes, inefficient query patterns, or insufficient resources. This guide covers the tools and techniques for diagnosing and fixing each.
explain() — Query Analysis
The explain() method reveals the query plan, indexes used, and execution statistics. It is the starting point for every performance investigation.
explain() verbosity levels
// Query planner only (fast — no execution)
db.users.find({ email: 'alice@example.com' }).explain()
// Full execution statistics (runs the query)
db.users.find({ email: 'alice@example.com' }).explain('executionStats')
// Compare all candidate plans
db.users.find({ email: 'alice@example.com' }).explain('allPlansExecution')Reading explain() Output
Field | Meaning |
|---|---|
stage: COLLSCAN | Full collection scan — no index used |
stage: IXSCAN | Index scan — index is being used |
stage: FETCH | Load full document after index lookup |
stage: SORT | In-memory sort — potential bottleneck |
nReturned | Documents returned to client |
totalDocsExamined | Documents read from storage (lower is better) |
totalKeysExamined | Index entries read |
executionTimeMillis | Total query execution time |
Before and after adding an index
// BEFORE — no index
// stage: COLLSCAN, totalDocsExamined: 500000, executionTimeMillis: 834
// After createIndex({ email: 1 })
// stage: IXSCAN, totalDocsExamined: 1, executionTimeMillis: 1The Database Profiler
The profiler logs slow queries to the system.profile capped collection.
Enable the profiler
// Log queries slower than 100ms
db.setProfilingLevel(1, { slowms: 100 })
// Query recent slow operations
db.system.profile
.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(10)
.pretty()Common Performance Anti-Patterns
Anti-Pattern | Problem | Fix |
|---|---|---|
No index on query/sort fields | COLLSCAN on large collection | Add targeted index |
$regex without start anchor (^) | Cannot use index | Use text index or Atlas Search |
Returning all fields | Excess data over network | Add projection |
Large skip() values | Scans and discards documents | Use cursor-based pagination |
Unbounded growing arrays | Document bloat, 16 MB limit | Use $slice or separate collection |
N+1 queries in app code | Many round trips to DB | Use $lookup or embed related data |
$or on different fields | May not leverage indexes well | Consider schema redesign |
Fetching to count | Loading docs just to count them | Use countDocuments() |
Connection Pooling
MongoDB drivers maintain a connection pool. Size the pool to match your application's concurrency — too small creates queuing, too large wastes server resources.
Node.js connection pool configuration
const client = new MongoClient(uri, {
maxPoolSize: 50, // max connections in pool
minPoolSize: 5, // keep 5 connections warm
maxIdleTimeMS: 30000, // close idle connections after 30s
serverSelectionTimeoutMS: 5000,
})Bulk Operations
bulkWrite() for high-throughput writes
db.products.bulkWrite(
[
{ insertOne: { document: { name: 'Widget A', price: 9.99 } } },
{ updateOne: { filter: { sku: 'B001' }, update: { $inc: { stock: -1 } } } },
{ deleteOne: { filter: { discontinued: true, stock: 0 } } },
],
{ ordered: false } // continue past errors for maximum throughput
)WiredTiger Cache
MongoDB's WiredTiger engine caches data in memory (default: 50% of RAM minus 1 GB). Your working set (active data + indexes) should fit in this cache.
Inspect cache utilisation
const stats = db.serverStatus().wiredTiger.cache
printjson({
maxCacheGB: (stats['maximum bytes configured'] / 1e9).toFixed(2),
usedCacheGB: (stats['bytes currently in the cache'] / 1e9).toFixed(2),
evictions: stats['pages evicted by application threads'],
})Atlas Real-Time Performance Panel
Ops/sec — read, write, command throughput
Query Targeting Ratio — documents examined per document returned (aim for 1.0)
Scan and Order — sorts without an index (should be near zero)
Connections — open connections to mongos/mongod
Cache utilisation — percentage of WiredTiger cache in use
Replication lag — secondary lag behind primary