The MongoDB Atlas Performance Advisor is flagging slow queries because the database is struggling to efficiently locate and retrieve the requested data, leading to increased latency and resource utilization.

Common Causes and Fixes for Slow Queries

1. Missing or Inappropriate Indexes:

  • Diagnosis: Run db.collection.explain("executionStats").find(<your_query_filter>) on your slow query. Look for totalKeysExamined and totalDocsExamined being significantly higher than nReturned. If indexName is null or "(null)", you’re performing a collection scan.
  • Fix: Identify fields used in your query’s filter, sort, and projection stages. Create an index that covers these fields. For example, if your query is db.users.find({ status: "active", age: { $gt: 30 } }).sort({ createdAt: -1 }), a compound index would be beneficial: db.users.createIndex({ status: 1, age: 1, createdAt: -1 }).
  • Why it works: Indexes are like a table of contents for your data. Instead of scanning the entire collection (document by document), MongoDB can use the index to quickly jump to the relevant documents, drastically reducing the number of documents it needs to examine.

2. Inefficient Compound Indexes (Order Matters):

  • Diagnosis: Even with indexes, if the order of fields in a compound index doesn’t match the query’s filter and sort order, it might not be used effectively. The explain() output will still show a high totalKeysExamined.
  • Fix: For queries with both filters and sorts, place the fields used for filtering before the fields used for sorting in the index definition. For equality matches, place them before range queries. For example, if querying db.orders.find({ customerId: "abc", amount: { $gt: 100 } }).sort({ orderDate: 1 }), create the index db.orders.createIndex({ customerId: 1, amount: 1, orderDate: 1 }).
  • Why it works: MongoDB’s compound indexes are ordered. They can efficiently serve queries that match the leading fields of the index. By ordering the index correctly, you maximize the chances that the index can satisfy both the filtering and sorting requirements without needing to examine extra data.

3. Large Documents and Field Selection (Projection):

  • Diagnosis: If your documents contain many fields, but your queries only need a few, MongoDB might still be reading and transferring more data than necessary. Check the bytesRead in your explain() output.
  • Fix: Use projection to specify only the fields you need in your query results. For example, db.products.find({ category: "electronics" }, { name: 1, price: 1, _id: 0 }).
  • Why it works: By explicitly including only the required fields ({ name: 1, price: 1 }) and excluding others (_id: 0), you reduce the amount of data MongoDB needs to retrieve from disk and send over the network, even if indexes are used.

4. Unbounded Sorts or Large Result Sets:

  • Diagnosis: Queries that sort a large number of documents without a preceding filter can be very slow. The explain() output will show a high totalDocsExamined and a large nReturned for the sort stage.
  • Fix: Ensure that any sort operation is preceded by a filter that significantly limits the number of documents being sorted. If possible, add an index that covers the filter and sort fields. For example, if you’re sorting recent posts, db.posts.find({ authorId: "user123" }).sort({ timestamp: -1 }) is better served by db.posts.createIndex({ authorId: 1, timestamp: -1 }).
  • Why it works: Sorting requires MongoDB to gather all documents matching the filter before it can order them. An index that covers both the filter and sort fields allows MongoDB to retrieve documents in the desired order directly from the index, avoiding a costly in-memory sort of many documents.

5. Inefficient $lookup (Left Outer Join):

  • Diagnosis: $lookup operations can be expensive, especially when joining large collections or when the join fields are not indexed. Your explain() output might show high totalDocsExamined on the "foreign" collection.
  • Fix: Ensure that the fields used in the $lookup’s localField and foreignField are indexed on their respective collections. For example, if joining orders with customers on customerId, ensure db.orders.createIndex({ customerId: 1 }) and db.customers.createIndex({ _id: 1 }) (assuming _id is the foreign key).
  • Why it works: Indexing the join fields allows MongoDB to efficiently find matching documents in the "foreign" collection without scanning it entirely for each document in the "local" collection.

6. Data Skew and Hot Shards:

  • Diagnosis: If your data distribution is highly uneven across shards (e.g., one shard has 90% of the data), queries targeting that shard will be slow. This is harder to diagnose directly from explain() and often requires looking at Atlas monitoring metrics for shard distribution and I/O.
  • Fix: Review your shard key. If it’s causing data skew, consider re-sharding with a different shard key or distributing your data more evenly. For existing data, you might need to write a script to redistribute documents.
  • Why it works: By distributing data more evenly across shards, query load is also distributed, preventing any single shard from becoming a bottleneck.

7. Excessive Use of find() without limit():

  • Diagnosis: If you’re fetching a very large number of documents with find() and not using limit(), the database has to process and buffer all those documents.
  • Fix: If you don’t need all the documents, use limit() to restrict the number of results. If you’re paginating, use a combination of skip() and limit(), but be aware of the performance implications of large skip() values. For deep pagination, consider range-based queries using indexed fields.
  • Why it works: limit() tells MongoDB to stop processing and returning documents once the specified count is reached, saving significant I/O and network resources.

The next error you’ll likely encounter after fixing these is WriteConcernMajorityNotSatisfied.

Want structured learning?

Take the full Mongodb course →