MongoDB查询优化之路:查询分析工具的使用

MongoDB中的查询分析工具可以帮助我们了解查询过程的详细信息,了解查询的每个步骤,校验索引是否符合预期等。

MongoDB查询分析常用工具有:explain() 和 hint()。

一、explain工具

explain 操作提供了查询信息,使用索引及查询统计等,有利于我们对索引的优化。

1
db.collection.explain()

接收三个值入参

  • queryPlanner:查询计划的选择器,首先进行查询分析,最终选择一个winningPlan,是explain返回的默认层面
  • executionStats:为执行统计层面,返回winningPlan的统计结果
  • allPlansExecution:为返回所有执行计划的统计,包括rejectedPlan

queryPlanner为我们选择出了winningPlan,而executionStats为我们统计了winningPlan的所有关键数据。

我们在查询优化的时候,主要是使用executionStats值。

1
2
3
4
5
6
7
8
9
10
11
12
"winningPlan" : {
"stage" : <STAGE1>,
...
"inputStage" : {
"stage" : <STAGE2>,
...
"inputStage" : {
"stage" : <STAGE3>,
...
}
}
}

explain 结果将查询计划以阶段树的形式呈现,每个阶段将其结果(文档或索引键)传递给父节点。中间节点操纵由子节点产生的文档或索引键。根节点是MongoDB从中派生结果集的最后阶段。

如果使用了executionStats入参,我们可以有下面三个结果输出:

  • queryPlanner:它详细说明查询优化器选择的计划,并列出拒绝的计划
  • executionStats:详细描述了最优计划和被拒绝的计划
  • serverInfo:它提供有关MongoDB实例的信息

1、queryPlanner主要参数解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"queryPlanner" : {
"plannerVersion" : <int>,
"namespace" : <string>, // 当前query所查询的表
"indexFilterSet" : <boolean>, // 当前query是否有indexfilter
"parsedQuery" : {
...
},
"winningPlan" : { // 查询优化器针对当前query所返回的最优执行计划的详细内容
"stage" : <STAGE1>, // 最优执行计划的stage
...
"inputStage" : { // 描述子输入阶段的文档,该阶段将文档或索引键提供给其父级
"stage" : <STAGE2>,
...
"inputStage" : {
...
}
}
},
"rejectedPlans" : [ // 其他执行计划(非最优而被查询优化器reject的)的详细返回
<candidate plan 1>,
...
]
}

这里重点关注winningPlan,它是MongoDB最终执行的它认为最优的查询,这里的stage将告诉你是采用了什么索引进行查询。

stage各类型值的意义
  • COLLSCAN:全表扫描
  • IXSCAN:索引扫描
  • FETCH:根据索引去检索指定document
  • SHARD_MERGE:各个分片返回数据进行merge
  • SORT:表明在内存中进行了排序
  • SORT_MERGE:表明在内存中进行了排序后再合并
  • LIMIT:使用limit限制返回数
  • SKIP:使用skip进行跳过
  • IDHACK:针对_id进行查询
  • SHARDING_FILTER:通过mongos对分片数据进行查询
  • COUNT:利用db.collection.count()之类进行count运算
  • COUNTSCAN:count不使用用Index进行count时的stage返回
  • COUNT_SCAN:count使用了Index进行count时的stage返回
  • SUBPLA:未使用到索引的$or查询的stage返回
  • TEXT:使用全文索引进行查询时候的stage返回

2、executionStats主要参数解析

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
"executionStats" : {
"executionSuccess" : <boolean>,
"nReturned" : <int>, // 返回结果数
"executionTimeMillis" : <int>, // 执行耗时(以毫秒为单位)
"totalKeysExamined" : <int>, // 索引扫描次数
"totalDocsExamined" : <int>, // 文档扫描次数
"executionStages" : { // 整个winningPlan执行树的详细信息,一个executionStages包含一个或者多个inputStages
"stage" : <STAGE1> // 扫描方式
"nReturned" : <int>, // 查询结果数量
"executionTimeMillisEstimate" : <int>, // 查询执行的估计时间(以毫秒为单位)
"works" : <int>, // 工作单元数,一个查询会分解成小的工作单元
"advanced" : <int>, // 优先返回数
"needTime" : <int>,
"needYield" : <int>,
"saveState" : <int>,
"restoreState" : <int>,
"isEOF" : <boolean>, // 查询执行是否已经到了数据流的末尾
"docsExamined" : <boolean>, // 文档检查数
...
"inputStage" : {
"stage" : <STAGE2>,
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
...
"inputStage" : {
...
}
}
},
// 所有查询计划的信息,包含最优和拒绝的
"allPlansExecution" : [ ... ]
}

这里重点关注nReturnedexecutionTimeMillistotalKeysExaminedtotalDocsExamined

理想状态下:

  • nReturned应该等于totalKeysExamined而且totalDocsExamined=0,这种情况相当于索引全覆盖,仅扫描索引,无需扫描文档,是最理想状态
  • nReturned、totalKeysExamined和totalDocsExamined三者相等,这种情况相当于检查的键数与返回的文档数相匹配,这意味着MongoDB只需检查索引键即可返回结果,MongoDB不必扫描所有文档或者多余文档,这个查询结果是非常高效的。
  • 如果有sort的时候,为了使得sort不在内存中进行,我们可以在保证nReturned等于totalDocsExamined的基础上,totalKeysExamined可以大于totalDocsExamined与nReturned,因为量级较大的时候内存排序非常消耗性能。

3、serverInfo主要参数

1
2
3
4
5
6
"serverInfo" : {
"host" : <string>, // 数据库主机信息
"port" : <int>, // 数据库端口
"version" : <string>, // 数据库版本
"gitVersion" : <string> // git版本号
}

4、实例分析

无索引查询name包含”其他”的文档集合:
1
2
3
db.collection.find({ 
'name': { $regex: '其他' }
}).explain('executionStats')
explain返回结果:
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
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$regex" : "其他"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$regex" : "其他"
}
},
"direction" : "forward"
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 27,
"executionTimeMillis" : 1379,
"totalKeysExamined" : 0,
"totalDocsExamined" : 83986,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$regex" : "其他"
}
},
"nReturned" : 27,
"executionTimeMillisEstimate" : 1332,
"works" : 83988,
"advanced" : 27,
"needTime" : 83960,
"needYield" : 0,
"saveState" : 677,
"restoreState" : 677,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 83986
}
},
"serverInfo" : {
"host" : "host",
"port" : 27017,
"version" : "4.0.6",
"gitVersion" : "caa42a1f75a56c7643d0b68d3880444375ec42e3"
},
"ok" : 1.0
}

结果说明:无索引下,MongoDB查询使用了全表扫描(COLLSCAN)的方式进行搜索,无索引遍历所以totalKeysExamined=0,遍历了83986个文档,整个执行耗时(executionTimeMillis)1.379秒,最终返回27条结果。

使用索引查询name包含”其他”的文档集合:
1
2
3
4
db.collection.createIndex({ 'name': 1 })
db.collection.find({
'name': { $regex: '其他' }
}).explain('executionStats')
explain返回结果:
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
103
104
105
106
107
108
109
110
111
112
113
114
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$regex" : "其他"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"name" : {
"$regex" : "其他"
}
},
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]"
]
}
}
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 27,
"executionTimeMillis" : 120,
"totalKeysExamined" : 83986,
"totalDocsExamined" : 27,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 27,
"executionTimeMillisEstimate" : 108,
"works" : 83987,
"advanced" : 27,
"needTime" : 83959,
"needYield" : 0,
"saveState" : 657,
"restoreState" : 657,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 27,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"name" : {
"$regex" : "其他"
}
},
"nReturned" : 27,
"executionTimeMillisEstimate" : 108,
"works" : 83987,
"advanced" : 27,
"needTime" : 83959,
"needYield" : 0,
"saveState" : 657,
"restoreState" : 657,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]"
]
},
"keysExamined" : 83986,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"host" : "host",
"port" : 27017,
"version" : "4.0.6",
"gitVersion" : "caa42a1f75a56c7643d0b68d3880444375ec42e3"
},
"ok" : 1.0
}

结果说明:对name进行索引加持后,MongoDB查询使用了索引扫描(FETCH+IXSCAN)的方式进行搜索,使用索引名为”name_1”的索引,遍历了83986个索引,遍历了27个文档,整个执行耗时(executionTimeMillis)0.124秒,最终返回27条结果,符合我们期望看的查询组合之一。

二、hint工具

1
db.collection.hint({ key: 1 })

虽然MongoDB查询优化器一般工作的很不错,但是也可以使用hint来强制MongoDB使用一个指定的索引,这种方法在某些情形下会提升性能。

hint()接受索引入参,告诉MongoDB使用入参的索引进行查询计划。

比如,有这样一个查询:

1
2
3
4
5
6
7
db.collection.find({
$or: [
{ 'name': { $regex: '文本' } },
{ 'name': { $regex: '按钮' } },
{ 'name': { $regex: '其他' } }
]
}).explain('executionStats')
explain返回结果:
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection",
"indexFilterSet" : false,
"parsedQuery" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"winningPlan" : {
"stage" : "SUBPLAN",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]",
"[/按钮/, /按钮/]",
"[/文本/, /文本/]"
]
}
}
}
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 831,
"executionTimeMillis" : 428,
"totalKeysExamined" : 83986,
"totalDocsExamined" : 831,
"executionStages" : {
"stage" : "SUBPLAN",
"nReturned" : 831,
"executionTimeMillisEstimate" : 381,
"works" : 83987,
"advanced" : 831,
"needTime" : 83155,
"needYield" : 0,
"saveState" : 664,
"restoreState" : 664,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 831,
"executionTimeMillisEstimate" : 381,
"works" : 83987,
"advanced" : 831,
"needTime" : 83155,
"needYield" : 0,
"saveState" : 664,
"restoreState" : 664,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 831,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"nReturned" : 831,
"executionTimeMillisEstimate" : 381,
"works" : 83987,
"advanced" : 831,
"needTime" : 83155,
"needYield" : 0,
"saveState" : 664,
"restoreState" : 664,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]",
"[/按钮/, /按钮/]",
"[/文本/, /文本/]"
]
},
"keysExamined" : 83986,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
},
"serverInfo" : {
"host" : "host",
"port" : 27017,
"version" : "4.0.6",
"gitVersion" : "caa42a1f75a56c7643d0b68d3880444375ec42e3"
},
"ok" : 1.0
}

结果说明:MongoDB查询使用了(SUBPLAN+FETCH+IXSCAN)的方式进行搜索,遍历了83986个文档,整个执行耗时(executionTimeMillis)0.428秒,最终返回831条结果。

使用hint强制使用指定索引
1
2
3
4
5
6
7
db.collection.find({
$or: [
{ 'name': { $regex: '文本' } },
{ 'name': { $regex: '按钮' } },
{ 'name': { $regex: '其他' } }
]
}).hint({ 'name': 1 }).explain('executionStats')
explain返回结果:
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection",
"indexFilterSet" : false,
"parsedQuery" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]",
"[/按钮/, /按钮/]",
"[/文本/, /文本/]"
]
}
}
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 831,
"executionTimeMillis" : 168,
"totalKeysExamined" : 83986,
"totalDocsExamined" : 831,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 831,
"executionTimeMillisEstimate" : 163,
"works" : 83987,
"advanced" : 831,
"needTime" : 83155,
"needYield" : 0,
"saveState" : 657,
"restoreState" : 657,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 831,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"$or" : [
{
"name" : {
"$regex" : "文本"
}
},
{
"name" : {
"$regex" : "按钮"
}
},
{
"name" : {
"$regex" : "其他"
}
}
]
},
"nReturned" : 831,
"executionTimeMillisEstimate" : 163,
"works" : 83987,
"advanced" : 831,
"needTime" : 83155,
"needYield" : 0,
"saveState" : 657,
"restoreState" : 657,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"\", {})",
"[/其他/, /其他/]",
"[/按钮/, /按钮/]",
"[/文本/, /文本/]"
]
},
"keysExamined" : 83986,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"host" : "host",
"port" : 27017,
"version" : "4.0.6",
"gitVersion" : "caa42a1f75a56c7643d0b68d3880444375ec42e3"
},
"ok" : 1.0
}

结果说明:MongoDB查询使用了(FETCH+IXSCAN)的方式进行搜索,与我们预期一致,遍历了831个文档,整个执行耗时(executionTimeMillis)0.428秒,最终返回831条结果。

在使用hint的查询,少了一个SUBPLAN父阶段,相当于减少了未使用到索引的$or查询stage返回。

三、结语

在查询分析中,我们需要关注查询的点:

  • 全表扫描(关键字:COLLSCAN、totalDocsExamined)。当一个操作(如查询、更新、删除等)需要全表扫描时,将非常占用CPU资源。如果这种情况比较频繁,建议对查询的字段建立索引的方式来优化。通过查看totalDocsExamined的值,可以查看到一个查询扫描了多少文档。该值越大,请求所占用的CPU开销越大。

  • 不合理的索引(关键字: IXSCAN、totalKeysExamined)索引不是越多越好,索引过多会影响写入、更新的性能。如果应用偏向于写操作,索引可能会影响性能。通过查看totalKeysExamined字段,可以查看到一个使用了索引的查询,扫描了多少条索引。该值越大,CPU开销越大。如果索引建立的不太合理,或者是匹配的结果很多。这样即使使用索引,查询开销也不会优化很多,执行的速度也会很慢。

  • 大量数据排序(关键字:SORT)当查询请求里包含排序的时候,如果排序无法通过索引满足,MongoDB会在查询结果中进行排序。而排序这个动作将非常消耗CPU资源,这种情况需要对经常排序的字段建立索引的方式进行优化。

  • 最期望看到的查询组合

    • FETCH+IDHACK
    • FETCH+IXSCAN
    • LIMIT+(FETCH+IXSCAN)
    • PROJECTION+IXSCAN
  • 最不期望看到的查询组合

    • COLLSCAN(全表扫)
    • SORT(使用sort但是无index)
    • COUNTSCAN(不使用索引进行count)