在MongoDB
中,explain
可以为查询提供大量的信息。对于慢查询来说,它是最重要的诊断工具之一。通过查看一个查询的 explain
输出,可以了解查询都使用了哪些索引以及是如何使用的。对于任何查询,都可以在末尾添加一个 explain
调用(就像添加 sort
或 limit
一样,但是 explain
必须是最后一个调用)。
最常见的 explain
输出有两种类型:使用索引的查询和未使用索引的查询。特殊类型的索引可能会创建略有不同的查询计划,但是大多数字段应该是相似的。此外,分片返回的是多个 explain
的集合,因为查询会在多个服务器端上执行。
最基本的 explain
类型是不使用索引的查询。如果一个查询不使用索引,则是因为它使用了 COLLSCAN
。
首先创建一个1000000个文档的集合:
> for (i=0; i<1000000; i++) {
... db.users.insertOne(
... {
... "i" : i,
... "username" : "name"+i,
... "age" : Math.floor(Math.random()*120),
... "created" : new Date()
... }
... );
... }
当不加索引的时候,explain
结果看起来会像下面这样:
> db.users.find({"age": 42}).explain('executionStats')
{
explainVersion: '1',
queryPlanner: {
namespace: 'videos.users',
indexFilterSet: false,
parsedQuery: { age: { '$eq': 42 } },
queryHash: '97A4421A',
planCacheKey: '97A4421A',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'COLLSCAN',
filter: { age: { '$eq': 42 } },
direction: 'forward'
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 8328,
executionTimeMillis: 388,
totalKeysExamined: 0,
totalDocsExamined: 1000000,
executionStages: {
stage: 'COLLSCAN',
filter: { age: { '$eq': 42 } },
nReturned: 8328,
executionTimeMillisEstimate: 20,
works: 1000001,
advanced: 8328,
needTime: 991672,
needYield: 0,
saveState: 1000,
restoreState: 1000,
isEOF: 1,
direction: 'forward',
docsExamined: 1000000
}
},
command: { find: 'users', filter: { age: 42 }, '$db': 'videos' },
serverInfo: {
host: 'localhost.localdomain',
port: 27117,
version: '7.0.12',
gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted'
},
ok: 1
}
接下来,我们创建2个复合索引:
> db.users.createIndex({'age': -1, 'username': 1}, {'name': 'idx_age_username'})
idx_age_username
> db.users.createIndex({'username': 1, 'age': -1}, {'name': 'idx_username_age'})
idx_username_age
在本例中:
接下来运行查询:
> db.users.find({"age": 42}).explain('executionStats')
{
explainVersion: '1',
queryPlanner: {
namespace: 'videos.users',
indexFilterSet: false,
parsedQuery: { age: { '$eq': 42 } },
queryHash: '97A4421A',
planCacheKey: '5786CCC4',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { age: -1, username: 1 },
indexName: 'idx_age_username',
isMultiKey: false,
multiKeyPaths: { age: [], username: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] }
}
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 8328,
executionTimeMillis: 18,
totalKeysExamined: 8328,
totalDocsExamined: 8328,
executionStages: {
stage: 'FETCH',
nReturned: 8328,
executionTimeMillisEstimate: 3,
works: 8329,
advanced: 8328,
needTime: 0,
needYield: 0,
saveState: 8,
restoreState: 8,
isEOF: 1,
docsExamined: 8328,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 8328,
executionTimeMillisEstimate: 0,
works: 8329,
advanced: 8328,
needTime: 0,
needYield: 0,
saveState: 8,
restoreState: 8,
isEOF: 1,
keyPattern: { age: -1, username: 1 },
indexName: 'idx_age_username',
isMultiKey: false,
multiKeyPaths: { age: [], username: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] },
keysExamined: 8328,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
},
command: { find: 'users', filter: { age: 42 }, '$db': 'videos' },
serverInfo: {
host: 'localhost.localdomain',
port: 27117,
version: '7.0.12',
gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted'
},
ok: 1
}
这个输出会首先告诉你使用了哪个索引:idx_age_username
。接下来是实际返回了多少文档:nReturned
。注意,这并不一定能反映出 MongoDB
在执行查询时做了多少工作(例如,它需要搜索多少索引和文档)。totalKeysExamined
描述了所扫描的索引项数量,totalDocsExamined
表示扫描了多少个文档。
executionTimeMillis
报告了查询的执行速度,即从服务器接收请求到发出响应的时间。可以看到,加索引后执行速度有了明显的提升。
接下来是对一些重要字段的详细介绍:
isMultiKey: false
本次查询是否使用了多键索引。
nReturned: 8328
本次查询返回的文档数量。
totalDocsExamined: 8328
MongoDB
按照索引指针在磁盘上查找实际文档的次数。如果查询中包含的查询条件不是索引的一部分,或者请求的字段没有包含在索引中,MongoDB
就必须查找每个索引项所指向的文档。
totalKeysExamined: 8328
如果使用了索引,那么这个数字就是查找过的索引条目数量。如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。
stage: 'IXSCAN'
MongoDB
是否可以使用索引完成本次查询。如果不可以,那么会使用COLLSCAN
表示必须执行集合扫描来完成查询。
在本例中,可以看出 MongoDB
使用索引找到了所有匹配的文档,因为totalKeysExamined
与 totalDocsExamined
是一样的。不过,此查询需要返回匹配文档中的每个字段,而索引中只包含了 age
字段和 username
字段。
needYield: 0
为了让写请求顺利进行,本次查询所让步(暂停)的次数。如果有写操作在等待执行,那么查询将定期释放它们的锁以允许写操作执行。在本次查询中,由于并没有写操作在等待,因此查询永远不会进行让步。
executionTimeMillis: 18
数据库执行本次查询所花费的毫秒数。这个数字越小越好。
indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] }
这描述了索引是如何被使用的,并给出了索引的遍历范围。在本例中,由于查询中的第一个子句是精确匹配,因此索引只需要查找 42 这个值就可以了。第二个索引键是一个自由变量,因为查询没有对它进行任何限制。因此,数据库会在符合 age: 42
的结果中查找用户名在负无穷("$minElement" : 1
)和正无穷("$maxElement" :1
)之间的数据。
如果发现 MongoDB
正在使用的索引与自己希望的不一致,则可以用 hint
强制其使用特定的索引。如果希望 MongoDB
在上面例子的查询中使用 {"username" : 1, "age" :1}
索引,则可以像下面这样做:
> db.users.find({"age" : 14, "username" : /.*/}).hint("idx_username_age")
hint
强制进行更改,那么应该在部署之前对这个查询执行 explain
。如果强制 MongoDB
在它不知道如何使用索引的查询上使用索引,则可能会导致查询效率比不使用索引时还要低。查看更多字段说明请查看官方文档。