Using MongoDB Explain with Mongoose
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.