← Back to DevBytes

MongoDB Database Performance: Profiling and Optimization

Understanding MongoDB Database Profiling

MongoDB database profiling is the process of collecting and analyzing detailed information about database operations—queries, writes, commands, and aggregation pipelines—that run against your MongoDB instance. The built-in profiler captures metadata such as execution time, documents scanned, documents returned, and the exact shape of each operation. This data becomes the foundation for identifying slow operations, understanding workload patterns, and pinpointing performance bottlenecks before they cascade into production outages.

At its core, profiling answers three critical questions: Which operations are running slowly? What resources are they consuming? And how can we make them faster? Without profiling, performance tuning becomes guesswork. With it, you gain empirical evidence to guide every optimization decision.

Why Profiling Matters

Slow database operations directly impact application responsiveness, user experience, and infrastructure costs. A single unoptimized query scanning millions of documents can saturate CPU, exhaust memory, and cause cascading latency across unrelated collections. Profiling matters because it transforms opaque slowness into actionable intelligence. It lets you catch problems early, validate the effectiveness of index changes, and establish performance baselines for capacity planning. Teams that profile regularly ship faster applications with fewer production incidents.

Enabling and Configuring the MongoDB Profiler

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

The MongoDB profiler operates at three distinct levels, controlled via the profile database command or the --profile startup option. Understanding each level helps you balance insight against overhead.

Profiler Levels Explained

Setting the Profiling Level

You can enable profiling dynamically without restarting the server. Connect to the admin database (or the database you want to profile) and run the following commands:

// Connect to the target database
use my_database;

// Enable level 1 profiling with a 200ms slow threshold
db.setProfilingLevel(1, { slowms: 200 });

// Verify the current settings
db.getProfilingStatus();

The getProfilingStatus() output confirms both the level and the threshold:

{
  "was": 0,
  "slowms": 200,
  "sampleRate": 1,
  "ok": 1
}

Understanding sampleRate

For high-throughput production systems, profiling every slow operation can still generate excessive log volume. The sampleRate parameter lets you capture only a fraction of slow operations. A value of 0.25 means approximately 25% of slow queries are logged. This reduces profiling overhead while still providing statistically representative data:

// Profile with a 100ms threshold, sampling 10% of slow operations
db.setProfilingLevel(1, { slowms: 100, sampleRate: 0.1 });

Per-Database vs. Global Profiling

The profiler settings apply per database. You can profile your primary application database aggressively while leaving system databases untouched. Each database stores its own profile data in a capped collection called system.profile within that database. To profile globally across all databases, you would need to configure each database individually or use a monitoring tool that aggregates server-wide metrics.

Reading and Analyzing Profile Data

All profiler output lands in the system.profile capped collection. This collection is created automatically when profiling is first enabled. Since it is capped, older entries are overwritten once the size limit is reached, so you should extract and archive important findings regularly.

Querying the system.profile Collection

The profile collection contains one document per logged operation. Here is how to query it effectively:

// Switch to the database you profiled
use my_database;

// Find the top 10 slowest queries
db.system.profile.find({
  millis: { $gt: 0 }
}).sort({ millis: -1 }).limit(10).pretty();

// Find queries that scanned many documents but returned few
// This pattern often indicates a missing index
db.system.profile.find({
  "docsExamined": { $gt: 10000 },
  "nreturned": { $lt: 10 }
}).sort({ docsExamined: -1 }).limit(5).pretty();

Anatomy of a Profile Entry

Each profile document contains rich metadata. Here is a typical entry for a slow find operation:

{
  "op": "query",
  "ns": "ecommerce.orders",
  "command": {
    "find": "orders",
    "filter": { "customerId": "abc123", "status": "shipped" },
    "sort": { "createdAt": -1 },
    "limit": 50
  },
  "keysExamined": 0,
  "docsExamined": 850000,
  "nreturned": 50,
  "hasSortStage": true,
  "millis": 1200,
  "planSummary": "COLLSCAN",
  "ts": ISODate("2025-03-15T14:22:11.442Z"),
  "client": "192.168.1.45",
  "user": "app_user@admin"
}

Key fields to focus on during analysis:

Aggregation Pipeline Analysis from Profile Data

Aggregation pipelines appear in the profiler as command operations. You can specifically filter for them:

// Find slow aggregation pipelines
db.system.profile.find({
  "command.aggregate": { $exists: true },
  "millis": { $gt: 500 }
}).sort({ millis: -1 }).limit(10).pretty();

Each aggregation stage is visible in the command document, letting you identify which specific stage—$group, $lookup, $unwind, etc.—causes the bottleneck.

Using explain() for Deep Query Inspection

While the profiler tells you what ran slowly, the explain() method tells you why. It reveals the query planner's decision process, the indexes considered, and the execution statistics for each stage. Use explain() proactively during development rather than waiting for slow queries to appear in production profiles.

Explain Verbosity Modes

The explain() method accepts a verbosity argument that controls the depth of output:

// Basic explain with execution stats
db.orders.explain("executionStats").find({
  customerId: "abc123",
  status: "shipped"
}).sort({ createdAt: -1 }).limit(50);

Reading the Explain Output

Focus on the executionStats section for actionable metrics:

{
  "executionStats": {
    "nReturned": 50,
    "executionTimeMillis": 1200,
    "totalKeysExamined": 0,
    "totalDocsExamined": 850000,
    "executionStages": {
      "stage": "SORT",
      "nReturned": 50,
      "sortByKey": { "createdAt": -1 },
      "inputStage": {
        "stage": "COLLSCAN",
        "filter": { "$and": [...] },
        "nReturned": 50,
        "docsExamined": 850000
      }
    }
  },
  "queryPlanner": {
    "winningPlan": {
      "stage": "SORT",
      "inputStage": {
        "stage": "COLLSCAN"
      }
    },
    "rejectedPlans": []
  }
}

The presence of COLLSCAN (collection scan) and SORT stages in the winning plan indicates a missing index. The planner had no better option. The rejected plans array is empty, confirming no indexes were applicable.

Explain for Aggregation Pipelines

You can explain aggregation pipelines to see stage-by-stage execution details:

db.orders.explain("executionStats").aggregate([
  { $match: { status: "shipped" } },
  { $group: { _id: "$category", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
]);

The output shows execution stats per stage, revealing whether $match used an index and how many documents flowed into the $group stage.

Index Optimization Strategies

Indexes are the single most powerful lever for MongoDB performance. A well-designed index can reduce a query from scanning millions of documents to examining exactly the needed ones in milliseconds. Profiling and explain output directly inform which indexes to create, modify, or remove.

Identifying Index Opportunities from Profile Data

Look for queries where docsExamined greatly exceeds nreturned. This ratio—called the selectivity ratio—should ideally be close to 1. A ratio of 1000 or higher means the query scanned 1000 documents to return just 1, wasting enormous resources.

// Find queries with poor selectivity
db.system.profile.find({
  "nreturned": { $gt: 0 },
  "$expr": {
    $gt: [{ $divide: ["$docsExamined", "$nreturned"] }, 100]
  }
}).sort({ millis: -1 }).limit(20);

The ESR Rule for Index Design

MongoDB indexes follow the ESR (Equality, Sort, Range) rule for optimal compound index key ordering:

For the earlier example query with { customerId: "abc123", status: "shipped" } and .sort({ createdAt: -1 }), the optimal index would be:

db.orders.createIndex({
  customerId: 1,   // Equality
  createdAt: -1    // Sort
});
// status is also equality but after the sort field it wouldn't be optimal
// If you frequently filter on both, consider:
db.orders.createIndex({
  customerId: 1,
  status: 1,
  createdAt: -1
});

Verifying Index Effectiveness

After creating an index, always run explain() again to confirm the planner uses it and that docsExamined drops dramatically:

// Before index: COLLSCAN, docsExamined: 850000
// After index creation, verify
db.orders.explain("executionStats").find({
  customerId: "abc123",
  status: "shipped"
}).sort({ createdAt: -1 }).limit(50);

// Now shows: IXSCAN, totalKeysExamined: 50, totalDocsExamined: 50

Index Intersection and Covering Queries

MongoDB can sometimes combine multiple indexes to satisfy a query via index intersection. More powerfully, a covering query is one where all returned fields are present in the index, eliminating the need to fetch full documents. Use projections to achieve this:

// Create an index that covers common lookups
db.orders.createIndex({
  customerId: 1,
  createdAt: -1,
  status: 1,
  amount: 1
});

// A covering query that never touches the documents
db.orders.find(
  { customerId: "abc123" },
  { _id: 0, status: 1, amount: 1, createdAt: 1 }
).sort({ createdAt: -1 });

The explain output for a covering query shows "totalDocsExamined": 0 even though keys are examined—a significant performance win.

Index Maintenance and Hidden Indexes

Over time, unused indexes waste memory and slow writes. Use the $indexStats aggregation to identify index usage patterns:

db.orders.aggregate([
  { $indexStats: {} }
]).toArray();

Each output document shows an index name and access counts. Indexes with zero accesses over a long period are candidates for removal. Before dropping, use hideIndex() to safely test the impact:

// Hide the index (queries won't use it, but writes still update it)
db.orders.hideIndex("customerId_1_createdAt_-1");

// Monitor performance for a week

// If no regression, drop it
db.orders.dropIndex("customerId_1_createdAt_-1");

Query and Schema Optimization Techniques

Beyond indexes, query structure and schema design significantly affect performance. Profiling reveals patterns that demand query rewrites or schema refactoring.

Optimizing Sort Operations

Sorting in memory is expensive; sorting on disk is catastrophic. The profiler's hasSortStage field flags queries that required a blocking sort. Eliminate sort stages by ensuring the index already provides documents in the desired order:

// This query triggers an in-memory sort because no index covers the sort
db.products.find({ category: "electronics" }).sort({ price: 1 });

// Create an index that covers both filter and sort
db.products.createIndex({ category: 1, price: 1 });
// Now the index returns documents pre-sorted by price within each category

Projection Optimization

Returning only needed fields reduces network transfer, deserialization overhead, and memory footprint. The profiler shows the full command, so you can audit queries that fetch entire documents unnecessarily:

// Instead of fetching entire documents
db.orders.find({ customerId: "abc123" });

// Fetch only needed fields
db.orders.find(
  { customerId: "abc123" },
  { _id: 0, orderDate: 1, total: 1, status: 1 }
);

Aggregation Pipeline Optimization

Pipeline order dramatically impacts performance. Apply these rules:

// Suboptimal pipeline: $group runs on all documents
db.orders.aggregate([
  { $group: { _id: "$category", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
]);

// Optimized: $match first to reduce input to $group
db.orders.aggregate([
  { $match: { amount: { $gt: 200 } } },
  { $group: { _id: "$category", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
]);

Bulk Operations and Write Optimization

The profiler also captures write operations. Look for patterns of many individual writes that could be batched:

// Instead of 1000 individual inserts
db.orders.insertOne({ /* doc */ });
db.orders.insertOne({ /* doc */ });
// ... repeated 1000 times

// Use bulk writes
const bulkOps = [];
for (const doc of documents) {
  bulkOps.push({ insertOne: { document: doc } });
}
db.orders.bulkWrite(bulkOps, { ordered: false });

For updates, use updateMany rather than looping with updateOne when applying the same change to multiple documents.

Schema Design for Performance

Profiling may reveal that certain queries require multiple round-trips or expensive joins ($lookup). Consider embedding related data when:

// Normalized: requires $lookup or multiple queries
// users collection: { _id: ObjectId, name: "Alice" }
// addresses collection: { _id: ObjectId, userId: ObjectId, street: "123 Main" }

// Denormalized for performance: embed frequently-accessed address
{
  _id: ObjectId,
  name: "Alice",
  addresses: [
    { street: "123 Main", city: "Springfield", primary: true },
    { street: "456 Oak", city: "Shelbyville", primary: false }
  ]
}

Proactive Performance Monitoring

Profiling is reactive by nature—it logs operations after they complete. Complement it with real-time monitoring to catch degrading performance before users notice.

Using db.currentOp() for Live Query Inspection

The currentOp command shows currently executing operations, letting you spot long-running queries in real time:

// Find operations running longer than 5 seconds
db.currentOp({
  "active": true,
  "secs_running": { $gt: 5 }
}).inprog.forEach(op => {
  printjson({
    opid: op.opid,
    ns: op.ns,
    secs_running: op.secs_running,
    command: op.command
  });
});

If you find a problematic operation, you can kill it by opid:

db.killOp(opid);

Server Metrics via db.serverStatus()

Server-wide metrics provide context for profiler findings. Key sections include:

const status = db.serverStatus();

// Connection utilization
printjson({ connections: status.connections });

// Operation counters
printjson({ opcounters: status.opcounters });

// Memory usage
printjson({ mem: status.mem });

// WiredTiger cache metrics
printjson({ wiredTiger: status.wiredTiger.cache });

High page eviction rates in the WiredTiger cache suggest insufficient memory or overly broad queries scanning too many documents.

Integrating with Monitoring Tools

For production systems, integrate profiler data with monitoring platforms. MongoDB Ops Manager, Atlas monitoring, or third-party tools like Datadog and Prometheus can consume profiler output, visualize trends, and alert on anomalies. Export profile data periodically using mongodump or aggregation queries to external analysis systems:

// Export recent slow queries to a dedicated analysis collection
db.system.profile.aggregate([
  { $match: { millis: { $gt: 200 }, ts: { $gte: new Date(Date.now() - 86400000) } } },
  { $out: "slow_queries_archive" }
]);

Best Practices for MongoDB Profiling and Optimization

1. Profile Strategically in Production

Use level 1 profiling with a thoughtful slowms threshold—typically 100-200ms for user-facing applications, higher for background jobs. Enable sampleRate on high-throughput systems to control log volume. Never run level 2 profiling in production.

2. Establish a Performance Baseline

After deploying indexes and query optimizations, capture explain outputs and profiler snapshots as a baseline. When performance regresses, compare current metrics against the baseline to isolate changes.

3. Create Indexes Based on Evidence, Not Assumption

Every index should be justified by profiler data or explain analysis showing a clear need. Avoid "just in case" indexing—unused indexes still consume memory and slow writes. Regularly audit index usage with $indexStats.

4. Monitor the Selectivity Ratio

Build dashboards or alerts around the ratio of docsExamined to nreturned. A ratio exceeding 100:1 for frequent queries demands immediate index investigation.

5. Test Index Changes with Hidden Indexes

Before dropping an index, hide it and observe performance for at least a full workload cycle (typically a week). Hidden indexes remain updated by writes but are invisible to the query planner, providing a safe rollback path.

6. Optimize Queries Before Scaling Hardware

A common anti-pattern is throwing more RAM or faster disks at slow queries. Profiling often reveals that a missing index or inefficient pipeline is the real culprit. Optimize first; scale hardware only when optimization is exhausted.

7. Use explain() During Code Review

Treat explain output as part of the development workflow. Any new query in a pull request should include an explain showing acceptable execution stats. This catches performance problems before they reach production.

8. Rotate and Archive Profile Data

The system.profile collection is capped and will overwrite older entries. Schedule regular exports or set up change streams to persist profile data for long-term trend analysis and compliance.

9. Combine Profiling with Real-Time Monitoring

Use currentOp and serverStatus alongside the profiler for a complete picture. Profiling tells you what happened; real-time monitoring tells you what is happening right now.

10. Document Performance Decisions

When you create an index or rewrite a query based on profiler findings, document the before/after metrics in a shared knowledge base. This builds institutional memory and helps onboard new team members to performance patterns.

Conclusion

MongoDB database profiling and optimization form a continuous feedback loop: profile to identify slow operations, analyze with explain to understand root causes, optimize through indexing and query restructuring, then profile again to validate improvements. The built-in profiler, combined with the explain method, gives you complete visibility into how your database executes every operation. By moving from reactive firefighting to proactive performance engineering—setting thoughtful profiling levels, auditing index usage, establishing baselines, and integrating performance checks into your development workflow—you build applications that remain fast and predictable at any scale. The tools are native, lightweight, and immediately available. The only remaining step is to enable profiling on your database and start listening to what the data tells you.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles