flyEn'blog

mongodb执行计划详解

本文记录了mongodb执行计划的各个参数及使用注意。

执行计划

MongoDB Query Plans 官方文档:https://docs.mongodb.com/v3.6/core/query-plans/#query-plans

查询计划逻辑图:

Diagram of query planner logic

MongoDB 3.0之后,explain的返回与使用方法与之前版本有很多变化。

现版本explain有三种模式:

  • queryPlanner(默认模式):MongoDB运行查询优化器对当前的查询进行评估并选择一个最佳的查询计划。
  • executionStats:MongoDB运行查询优化器对当前的查询进行评估并选择一个最佳的查询计划进行执行,在执行完毕后返回这个最佳执行计划执行完成时的相关统计信息,对于那些被拒绝的执行计划,不返回其统计信息。
  • allPlansExecution:该模式包括上述2种模式的所有信息,即按照最佳的执行计划执行以及列出统计信息,如果存在其他候选计划,也会列出这些候选的执行计划。

比如说 db.trade_order.find().explain()

不填默认queryPlanner模式,可指定.explain("executionStats")

queryPlanner

并不会去真正进行query语句查询,而是针对query语句进行执行计划分析并选出winning plan。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// db.usertable.find({"w": 1}).explain("queryPlanner")
// 举个执行计划响应结果的例子:
{
"queryPlanner":{
"plannerVersion":1,
"namespace":"game_db.game_user", // 该query所查询的表
"indexFilterSet":false, // 是否应用了index filter
"parsedQuery":{ // 解析查询 查询条件
"w":{
"$eq":1
}
},
"winningPlan":{ // 查询优化器针对该query所返回的最优执行计划的详细内容
"stage":"FETCH", // 最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档
"inputStage":{ // explain.queryPlanner.winningPlan.stage的child stage,此处是IXSCAN,表示进行的是index scanning
"stage":"IXSCAN", // 索引查找
"keyPattern":{ // 所扫描的index内容,此处是w:1与n:1。
"w":1,
"n":1
},
"indexName":"w_1_n_1", // winning plan所选用的index。
"isMultiKey":false, // 本次查询是否使用了多键、复合索引
"direction":"forward", // 此query的查询顺序,此处是forward,如果用了.sort({w:-1})将显示backward。
"indexBounds":{ // winningplan所扫描的索引范围,此处查询条件是w:1,使用的index是w与n的联合索引,故w是[1.0,1.0]而n没有指定在查询条件中,故是[MinKey,MaxKey]。
"w":[
"[1.0, 1.0]"
],
"n":[
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans":[ // 其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。
{
"stage":"FETCH",
"inputStage":{
"stage":"IXSCAN",
"keyPattern":{
"w":1,
"v":1
},
"indexName":"w_1_v_1",
"isMultiKey":false,
"direction":"forward",
"indexBounds":{
"w":[
"[1.0, 1.0]"
],
"v":[
"[MinKey, MaxKey]"
]
}
}
}
]
},
"serverInfo" : { // 服务器信息,包含主机名和ip,mongodb的版本等
"host" : "ALI-SZ-VT-TEST001",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}

上例的执行计划返回结果中,queryPlanner.winningPlan.stage参数,常用的有:

类型 描述
COLLSCAN 全表扫描
IXSCAN 索引扫描
FETCH 根据索引检索指定的文档
SHARD_MERGE 将各个分片的返回结果进行merge
SORT 表明在内存中进行了排序
LIMIT 使用limit限制返回结果数量
SKIP 使用skip进行跳过
IDHACK 针对_id字段进行查询
SHANRDING_FILTER 通过mongos对分片数据进行查询
COUNT 利用db.coll.explain().count()进行count运算
COUNTSCAN count不使用用index进行count时的stage返回
COUNT_SCAN count使用了Index进行count时的stage返回
SUBPLA 未使用到索引的$or查询的stage返回
TEXT 使用全文索引进行查询时候的stage返回
PROJECTION 限定返回字段时候stage的返回

上表中,加粗部分为最常用的。

executionStats

executionStats返回的是最优计划的详细的执行信息。

注意,必须是executionStats或者allPlansExecution模式中,才返回executionStats结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 比之上面的,多了一个executionStats返回
"executionStats" : {
"executionSuccess" : true, // 是否执行成功
"nReturned" : 2, // 此query匹配到的文档数
"executionTimeMillis" : 0, // 查询计划选择和查询执行所需的总时间,单位:毫秒
"totalKeysExamined" : 2, // 扫描的索引条目数
"totalDocsExamined" : 2, // 扫描的文档数
"executionStages" : { // 最优计划完整的执行信息
"stage" : "FETCH", // 根据索引结果扫描具体文档
"nReturned" : 2, // 由于是 FETCH,这里的结果跟上面的nReturned结果一致
"executionTimeMillisEstimate" : 0,
"works" : 3, // 查询执行阶段执行的“工作单元”的数量。 查询执行阶段将其工作分为小单元。 “工作单位”可能包括检查单个索引键,从集合中提取单个文档,将投影应用于单个文档或执行内部记账
"advanced" : 2, // 返回到父阶段的结果数
"needTime" : 0, // 未将中间结果返回给其父级的工作循环数
"needYield" : 0, // 存储层请求查询系统产生锁定的次数
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1, // 执行阶段是否已到达流的结尾
"invalidates" : 0,
"docsExamined" : 2, // 跟totalDocsExamined结果一致
"alreadyHasObj" : 0,
"inputStage" : { // 一个小的工作单元,一个执行计划中,可以有一个或者多个inputStage
"stage" : "IXSCAN",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 3,
"advanced" : 2,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"age" : 1
},
"indexName" : "s4_id1",
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"(20.0, inf.0]"
]
},
"keysExamined" : 2,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}

执行计划的结果是以阶段树的形式呈现,每个阶段将其结果(文档或者索引键)传递给父节点,叶子节点访问集合或者索引,中间节点操纵由子节点产生的文档或者索引键,最后汇总到根节点。

allPlansExecution

allPlansExecution模式返回了更为详细的执行计划结果,这里不再赘述。

IndexFilter

https://docs.mongodb.com/v3.6/core/query-plans/#index-filters

索引过滤器(IndexFilter)决定了查询优化器对于某一类型的查询将如何使用index,且仅影响指定的查询类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// 首先,集合 s4 有三个索引
db.s4.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_", // MongoDB自动创建的索引
"ns" : "t1.s4" // 表名
},
{
"v" : 2,
"key" : {
"age" : 1
},
"name" : "s4_id1", // 我们手动创建的索引
"ns" : "t1.s4"
},
{
"v" : 2,
"key" : {
"age" : -1
},
"name" : "s4_id2", // 我们手动创建的索引
"ns" : "t1.s4"
}
]

// 在查询时,MongoDB会自动的帮我们选择使用哪个索引
// 以下两种查询查询优化器都会选择使用 s4_id2 索引
db.s4.explain().find({"age": {"$gt":18}})
db.s4.explain().find({"age": 18})
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "t1.s4",
"indexFilterSet" : false, // 注意这个值是false
"parsedQuery" : {
"age" : {
"$eq" : 18
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : -1
},
"indexName" : "s4_id2", // 查询优化器选择了s4_id2这个索引
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"[18.0, 18.0]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1
},
"indexName" : "s4_id1", // 并将s4_id1索引排除
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"[18.0, 18.0]"
]
}
}
}
]
},
"serverInfo" : {
"host" : "cs",
"port" : 27017,
"version" : "3.6.12",
"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
},
"ok" : 1
}

那如果有些应用场景下需要特定索引,而不是用查询优化器帮助我们选择的索引,该怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 通过 hint 明确指定索引
db.s4.explain().find({"age": {"$gt":18}}).hint("s4_id1")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "t1.s4",
"indexFilterSet" : false,
"parsedQuery" : {
"age" : {
"$gt" : 18
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1
},
"indexName" : "s4_id1", // hint告诉查询优化器选择使用指定的索引s4_id1
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"(18.0, inf.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "cs",
"port" : 27017,
"version" : "3.6.12",
"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
},
"ok" : 1
}

问题解决了。

但我们还有其他的解决方式,就是提前声明为某一类查询指定特定的索引(索引必须存在),当有这类查询时,自动使用特定索引,或者从指定的几个索引中选择一个,而不是通过hint显式指定,且不影响其他类型的查询,这就用到了IndexFilter。

1
2
3
4
5
6
7
// 创建IndexFilter
db.runCommand({
planCacheSetFilter: "s4",
query: {"age": 18}, // 只要查询类型是 age等于某个值,该IndexFilter就会被应用,并不特指18
// indexes:[{age: 1}] // 跟下面一行等价
indexes:["s4_id1"] // 可以有多个备选索引
})

上面的IndexFilter的意思是,当查询集合s4时,且查询类型是age等于某个值时(只作用于该类型的查询),就从indexes数组中选择一个索引,或者从多个备选索引中,选择一个最优的索引,当然,你也可以像示例中一样,只写一个索引s4_id1

之后再来执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 对于查询是 age 等于某个值这种类型的查询,查询优化器都会选择我们指定的IndexFilter
db.s4.explain().find({"age": 18})
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "t1.s4",
"indexFilterSet" : true, // IndexFilter被应用
"parsedQuery" : {
"age" : {
"$eq" : 18
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1
},
"indexName" : "s4_id1", // 使用了IndexFilter指定的索引
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"[18.0, 18.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "cs",
"port" : 27017,
"version" : "3.6.12",
"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
},
"ok" : 1
}

注意,如果某一类型的查询设定了IndexFilter,那么执行时通过hint指定了其他的index,查询优化器将会忽略hint所设置index,仍然使用indexfilter中设定的查询计划,也就是下面这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
db.s4.explain().find({"age": 18}).hint("s4_id2")  // 希望在查询时使用 s4_id2 索引
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "t1.s4",
"indexFilterSet" : true, // 创建的indexFilter被应用
"parsedQuery" : {
"age" : {
"$eq" : 18
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1
},
"indexName" : "s4_id1", // 忽略hint指定的s4_id2,选择IndexFilter指定的索引
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"[18.0, 18.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "cs",
"port" : 27017,
"version" : "3.6.12",
"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
},
"ok" : 1
}

另外,IndexFilter不会影响其他类型的查询,如下面的查询类型,查询优化器还是按照原来的规则选择最优计划:

1
2
db.s4.explain().find({"age": {"$gt":18}})
db.s4.explain().find({"age": {"$gt":18}}).hint("s4_id1")

IndexFilter的其他操作:

1
2
3
4
5
6
7
8
9
// 查看指定集合中的IndexFilter数组
db.runCommand({planCacheListFilters: "s4"})

// 删除IndexFilter
db.runCommand({
planCacheClearFilters: "s4",
// query: {"age": 18}, // 对应创建IndexFilter时的注释的那一行
indexes:["s4_id1"]
})

当然,在有些情况下,你删除指定的IndexFilter会失败,终极的解决办法是——重启MongoDB服务。

小结:

  • IndexFilter为指定类型的查询提前设置使用某一个或者从多个备选索引中选择指定索引。
  • IndexFilter指定的索引必须存在,如果索引不存在——也没事!全表扫描呗!
  • IndexFilter的优先级高于hint,这点是在生产中要注意的。
  • IndexFilter一定程度上解决了某些问题,但它使用相对麻烦,谨慎使用!
Fork me on GitHub