Using MongoDB Explain with Mongoose

Jun 3, 2020

In MongoDB, the explain command tells the MongoDB server to return stats about how it executed a query, rather than the results of the query. Mongoose queries have an explain() function that converts a query into an explain().

const Character = mongoose.model('Character', mongoose.Schema({
  name: String,
  age: Number,
  rank: String
}));

await Character.create([
  { name: 'Jean-Luc Picard', age: 59, rank: 'Captain' },
  { name: 'William Riker', age: 29, rank: 'Commander' },
  { name: 'Deanna Troi', age: 28, rank: 'Lieutenant Commander' },
  { name: 'Geordi La Forge', age: 29, rank: 'Lieutenant' },
  { name: 'Worf', age: 24, rank: 'Lieutenant' }
]);

const explain = await Character.find({ name: /Picard/ }).explain().
  then(res => res[0]);
// Object describing how MongoDB planned to execute the query
explain.queryPlanner;
// Object containing stats about how MongoDB executed the query
explain.executionStats;

Reading the queryPlanner Output

The queryPlanner object contains more detailed information about how MongoDB decided to execute the query. For example, below is the queryPlanner object from the above explain() call.

{
  plannerVersion: 1,
  namespace: 'test.characters',
  indexFilterSet: false,
  parsedQuery: { name: { '$regex': 'Picard' } },
  winningPlan: {
    stage: 'COLLSCAN',
    filter: { name: { '$regex': 'Picard' } },
    direction: 'forward'
  },
  rejectedPlans: []
}

The most important piece of information is the winningPlan property, that contains the information about the plan MongoDB decided on to execute the query. In practice, winningPlan is useful for checking whether MongoDB used an index for the query or not.

A query plan is a list of stages used to identify the documents that match the query. The above plan has only one stage, 'COLLSCAN', which means MongoDB executed a full collection scan to answer the query. A collection scan means MongoDB searched through every document in the 'characters' collection to see if name matched the given query.

Query plans get more sophisticated when you introduce indexes. For example, suppose you add an index on name as shown below.

await Character.collection.createIndex({ name: 1 });

const explain = await Character.find({ name: 'Jean-Luc Picard' }).explain().
  then(res => res[0]);
// Object describing how MongoDB planned to execute the query
explain.queryPlanner;

The queryPlanner output looks like this:

{
  plannerVersion: 1,
  namespace: 'test.characters',
  indexFilterSet: false,
  parsedQuery: { name: { '$eq': 'Jean-Luc Picard' } },
  winningPlan: {
    stage: 'FETCH',
    inputStage: {
      stage: 'IXSCAN',
      keyPattern: { name: 1 },
      indexName: 'name_1',
      isMultiKey: false,
      multiKeyPaths: { name: [] },
      isUnique: false,
      isSparse: false,
      isPartial: false,
      indexVersion: 2,
      direction: 'forward',
      indexBounds: { name: [ '["Jean-Luc Picard", "Jean-Luc Picard"]' ] }
    }
  },
  rejectedPlans: []
}

The winningPlan property is a recursive structure: winningPlan points to the last stage in the winning query plan, and each stage has an inputStage property that described the previous stage.

In the above plan, there are two stages: 'IXSCAN' and 'FETCH'. That means first MongoDB used the { name: 1 } index to identify which documents matched the query, and then fetched the individual documents.

Reading the executionStats Output

The executionStats output is more complex than queryPlanner: it includes stats about how long each stage took and how many documents each stage scanned.

For example, below is the executionStats output for a simple collection scan:

{
  executionSuccess: true,
  nReturned: 1,
  executionTimeMillis: 0,
  totalKeysExamined: 0,
  totalDocsExamined: 5,
  executionStages: {
    stage: 'COLLSCAN',
    filter: { name: { '$regex': 'Picard' } },
    nReturned: 1,
    executionTimeMillisEstimate: 0,
    works: 7,
    advanced: 1,
    needTime: 5,
    needYield: 0,
    saveState: 0,
    restoreState: 0,
    isEOF: 1,
    direction: 'forward',
    docsExamined: 5
  },
  allPlansExecution: []
}

The important details to note here are the top-level executionTimeMillis and totalDocsExamined properties. executionTimeMillis is the amount of time MongoDB spent executing the query, and totalDocsExamined is the number of documents MongoDB had to look at to answer the query.

Keep in mind that executionTimeMillis does not include network latency or time spent blocked behind a slow train. Just because executionTimeMillis is small doesn't mean that the end user saw the result instantaneously.

When you have an index and multiple stages, executionStats breaks down the approximate execution time and number of documents scanned per stage. Below is the executionStats for a query with an index, with some of the less important details excluded for brevity:

{
  executionSuccess: true,
  nReturned: 1,
  executionTimeMillis: 2,
  totalKeysExamined: 1,
  totalDocsExamined: 1,
  executionStages: {
    stage: 'FETCH',
    nReturned: 1,
    executionTimeMillisEstimate: 0,
    // ...
    docsExamined: 1,
    // ...
    inputStage: {
      stage: 'IXSCAN',
      nReturned: 1,
      executionTimeMillisEstimate: 0,
      // ...
    }
  },
  allPlansExecution: []
}

The above executionStats output says that there were two stages: 'IXSCAN' and 'FETCH'. The 'IXSCAN' stage executed in 0ms and resulted in one document being sent to the 'FETCH' stage. The 'FETCH' stage examined 1 document, and returned 1 document, which was the final result of the query.


Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!

Did you find this tutorial useful? Say thanks by starring our repo on GitHub!

More Mongoose Tutorials