性能优化
知道 index 和执行计划之后,我们要学会分析执行计划。选择出最优秀的索引方案。
索引准则
-
避免全表扫描
-
避免过多索引。如无必要,不增索引。
关注点
关注点1:执行时间
其中有3个executionTimeMillis,分别是
executionStats.executionTimeMillis
该query的整体查询时间
executionStats.executionStages.executionTimeMillis
该查询根据index去检索document获取2条具体数据的时间
executionStats.executionStages.inputStage.executionTimeMillis
该查询扫描 2 行index所用时间
这三个值我们都希望越少越好,那么是什么影响这这三个返回值呢?
抛开硬件因素等不谈,我们来进行下一层的剥离。
关注点2:返回的条数
这里主要谈3个返回项,nReturned,totalKeysExamined与totalDocsExamined,分别代表该条查询返回的条目、索引扫描条目和文档扫描条目。
很好理解,这些都直观的影响到executionTimeMillis。我们需要扫描的越少速度越快。
对于一个查询, 我们最理想的状态是
nReturned=totalKeysExamined & totalDocsExamined=0
(cover index,仅仅使用到了index,无需文档扫描,这是最理想状态。)
或者
nReturned=totalKeysExamined=totalDocsExamined
(需要具体情况具体分析)
(正常index利用,无多余index扫描与文档扫描。)
如果有sort的时候,为了使得sort不在内存中进行,我们可以在保证 nReturned=totalDocsExamined 的基础上,totalKeysExamined可以大于totalDocsExamined与nReturned,因为量级较大的时候内存排序非常消耗性能。
后面我们会针对例子来进行分析。
关注点3:Stage状态分析
那么又是什么影响到了totalKeysExamined与totalDocsExamined呢?
就是Stage的类型,Stage的具体含义在上文中有提及,如果认真看的同学就不难理解为何Stage会影响到totalKeysExamined 和totalDocsExamined从而影响executionTimeMillis了。
此前有讲解过stage的类型,这里不再赘述。
普通查询
- 希望看到
Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FILTER+ixscan
...
- 不希望看到
COLLSCAN(全表扫),SORT(使用sort但是无index),不合理的SKIP,SUBPLA(未用到index的$or)
对于 count
- 希望看到的有
COUNT_SCAN
- 不希望看到的有
COUNTSCAN
Explain 实例
初始化测试数据
db.explain.insertMany([
{ "_id" : ObjectId("55b86d6bd7e3f4ccaaf20d70"), "a" : 1, "b" : 1, "c" : 1 },
{ "_id" : ObjectId("55b86d6fd7e3f4ccaaf20d71"), "a" : 1, "b" : 2, "c" : 2 },
{ "_id" : ObjectId("55b86d72d7e3f4ccaaf20d72"), "a" : 1, "b" : 3, "c" : 3 },
{ "_id" : ObjectId("55b86d74d7e3f4ccaaf20d73"), "a" : 4, "b" : 2, "c" : 3 },
{ "_id" : ObjectId("55b86d75d7e3f4ccaaf20d74"), "a" : 4, "b" : 2, "c" : 5 },
{ "_id" : ObjectId("55b86d77d7e3f4ccaaf20d75"), "a" : 4, "b" : 2, "c" : 5 },
{ "_id" : ObjectId("55b879b442bfd1a462bd8990"), "a" : 2, "b" : 1, "c" : 1 },
{ "_id" : ObjectId("55b87fe842bfd1a462bd8991"), "a" : 1, "b" : 9, "c" : 1 },
{ "_id" : ObjectId("55b87fe942bfd1a462bd8992"), "a" : 1, "b" : 9, "c" : 1 },
{ "_id" : ObjectId("55b87fe942bfd1a462bd8993"), "a" : 1, "b" : 9, "c" : 1 }
])
业务需求
查询 a=1,b 的值小于3,且按照 c 倒叙排序
db.d.find({a:1,b:{$lt:3}}).sort({c:-1})
没有索引的情况
db.explain.find({a:1,b:{$lt:3}}).sort({c:-1}).explain('executionStats')
结果如下:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.explain",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"a" : {
"$eq" : 1.0
}
},
{
"b" : {
"$lt" : 3.0
}
}
]
},
"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {
"c" : -1.0
},
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"a" : {
"$eq" : 1.0
}
},
{
"b" : {
"$lt" : 3.0
}
}
]
},
"direction" : "forward"
}
}
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 2,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 10,
"executionStages" : {
"stage" : "SORT",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 16,
"advanced" : 2,
"needTime" : 13,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"c" : -1.0
},
"memUsage" : 126,
"memLimit" : 33554432,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 13,
"advanced" : 2,
"needTime" : 10,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"a" : {
"$eq" : 1.0
}
},
{
"b" : {
"$lt" : 3.0
}
}
]
},
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 12,
"advanced" : 2,
"needTime" : 9,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 10
}
}
}
},
"serverInfo" : {
"host" : "c120a2890d51",
"port" : 27017,
"version" : "4.0.4",
"gitVersion" : "f288a3bdf201007f3693c58e140056adf8b04839"
},
"ok" : 1.0
}
此处的条数较少,暂时不关心时间。
我们开始看条数和 executionStats。
"nReturned" : 2, -- 符合的条件的返回为2条。
"executionTimeMillis" : 0,
"totalKeysExamined" : 0, -- 未命中索引
"totalDocsExamined" : 10, -- 扫描了所有记录
- 排序
executionStats.executionStages.sort=SORT
没有命中索引
- 内存占用
"memUsage" : 126, -- 占用的内存
"memLimit" : 33554432, -- 内存限制
- 扫描类型
executionStages.inputStage.stage=COLLSCAN
,全表扫描,扫描条件为
"filter" : {
"$and" : [
{
"a" : {
"$eq" : 1.0
}
},
{
"b" : {
"$lt" : 3.0
}
}
]
}
优化步骤
排序字段优化
排序添加索引
db.explain.createIndex({c: 1})
查看执行计划
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 2,
"executionTimeMillis" : 4,
"totalKeysExamined" : 10,
"totalDocsExamined" : 10,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"a" : {
"$eq" : 1.0
}
},
{
"b" : {
"$lt" : 3.0
}
}
]
},
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 11,
"advanced" : 2,
"needTime" : 8,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 10,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 10,
"executionTimeMillisEstimate" : 0,
"works" : 11,
"advanced" : 10,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"c" : 1.0
},
"indexName" : "c_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"c" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"c" : [
"[MaxKey, MinKey]"
]
},
"keysExamined" : 10,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
结果分析
- 排序
Stage没有了SORT,因为我们sort字段有了index,但是由于查询还是没有index,故totalDocsExamined还是10,但是由于sort用了index,totalKeysExamined也是10
- 查询
还是扫描了所有的数据,需要进一步优化。
"nReturned" : 2,
"executionTimeMillis" : 4,
"totalKeysExamined" : 10,
"totalDocsExamined" : 10,
查询优化
新建索引
删除原来的索引。
db.explain.dropIndexes();
db.explain.createIndex({a:1, b:1, c:1});
执行计划
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 2,
"executionTimeMillis" : 6,
"totalKeysExamined" : 2,
"totalDocsExamined" : 2,
"executionStages" : {
"stage" : "SORT",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 7,
"advanced" : 2,
"needTime" : 4,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"c" : -1.0
},
"memUsage" : 126,
"memLimit" : 33554432,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 4,
"advanced" : 2,
"needTime" : 1,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 3,
"advanced" : 2,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 2,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 3,
"advanced" : 2,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"a" : 1.0,
"b" : 1.0,
"c" : 1.0
},
"indexName" : "a_1_b_1_c_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"a" : [],
"b" : [],
"c" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"a" : [
"[1.0, 1.0]"
],
"b" : [
"[-inf.0, 3.0)"
],
"c" : [
"[MinKey, MaxKey]"
]
},
"keysExamined" : 2,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
}
}
结果分析
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 2,
"executionTimeMillis" : 6,
"totalKeysExamined" : 2,
"totalDocsExamined" : 2,
"executionStages" : {
"stage" : "SORT",
- 查询
查询已经满足我们的条件了。
- 排序
但是排序为 SORT,属于内存排序,不符合我们的预期。
sort 优化方式1
新建索引
删除原来的索引。
db.explain.dropIndexes();
db.explain.createIndex({a:1, c:1, b:1});
执行计划
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 2,
"executionTimeMillis" : 5,
"totalKeysExamined" : 4,
"totalDocsExamined" : 2,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 5,
"advanced" : 2,
"needTime" : 2,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 2,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 5,
"advanced" : 2,
"needTime" : 2,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"a" : 1.0,
"c" : 1.0,
"b" : 1.0
},
"indexName" : "a_1_c_1_b_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"a" : [],
"c" : [],
"b" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"a" : [
"[1.0, 1.0]"
],
"c" : [
"[MaxKey, MinKey]"
],
"b" : [
"(3.0, -inf.0]"
]
},
"keysExamined" : 4,
"seeks" : 3,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
结果分析
"nReturned" : 2,
"executionTimeMillis" : 5,
"totalKeysExamined" : 4,
"totalDocsExamined" : 2,
"executionStages" : {
"stage" : "FETCH",
nReturned为2,返回2条记录
totalKeysExamined为4,扫描了4个index
totalDocsExamined为2,扫描了2个docs
虽然不是nReturned=totalKeysExamined=totalDocsExamined,但是Stage无Sort,即利用了index进行排序,而非内存,这个性能的提升高于多扫几个index的代价。
小技巧
综上可以有一个小结论,当查询覆盖精确匹配,范围查询与排序的时候
{精确匹配字段,排序字段,范围查询字段}
这样的索引排序会更为高效。
可以指定命中的索引
db.inventory.createIndex( { quantity: 1, type: 1 } )
db.inventory.createIndex( { type: 1, quantity: 1 } )
指定出发第一个索引
db.inventory.find(
{ quantity: { $gte: 100, $lte: 300 }, type: "food" }
).hint({ quantity: 1, type: 1 }).explain("executionStats")
性能优化拓展阅读
https://docs.mongodb.com/manual/core/query-optimization/
https://docs.mongodb.com/manual/core/query-plans/
https://docs.mongodb.com/manual/tutorial/optimize-query-performance-with-indexes-and-projections/
https://docs.mongodb.com/manual/tutorial/create-indexes-to-support-queries/