1-MongoDB - 执行计划

about

MongoDB3.6.12 + centos7.9

MongoDB提供了执行计划来帮助我们分析,一条语句执行情况,但这条语句不会真正的被执行,只是为优化或者其他操作提供依据。

一般的,执行计划通常在索引中应用较多,通过执行计划查看创建的索引的应用情况。

由于MongoDB在3.0版本后对执行计划做了优化,这里也仅针对MongoDB3.0以后的执行计划进行讨论,而接下来的示例中以MongoDB3.6.12版本为例。

Query Plan

Query Plan

query shape:即查询谓词,排序和投影规范的组合。投影意味着仅选择必要的数据,而不是选择文档的整个数据。 如果文档有5个字段,而我们只需要显示3个字段,则仅从其中选择3个字段。 MongoDB提供了一些投影运算符,可以帮助我们实现该目标。

对于查询来说,MongoDB查询优化器处理查询,并在给定可用索引的情况下为查询选择最有效的查询计划。然后,查询系统将在每次查询运行时使用此查询计划。

查询优化器仅缓存那些具有多个可行计划的查询形状的计划。

对于每个查询,查询计划者都会在查询计划缓存中搜索适合query shape的条目。如果没有匹配的条目,查询计划器将生成候选计划,试用该候选计划并在试用期内进行评估。查询计划器选择一个获胜计划并进行缓存,然后使用它来生成结果文档。

如果存在匹配的计划,则查询计划程序将基于该条目生成计划,并通过一种replanning机制评估其性能。该机制pass/fail根据计划的性能做出决定,并保留或逐出缓存条目。逐出时,查询计划者将使用常规计划过程选择一个新计划并将其缓存。查询计划者执行计划并返回查询的结果文档。

下图说明了查询计划程序逻辑:

图片来源:https://docs.mongodb.com/v3.6/core/query-plans/#query-plans

基本使用

MongoDB中的执行计划基本用法如下:

// 语法参考
db.collection.explain(verbosity).<method(...)>

// method 支持查看以下语句的执行计划
aggregate()
count()
distinct()
find()
group()
remove()
update()

// 通过help查看帮助信息
db.collection.explain().help()
db.collection.explain().find().help()

// 你也可以这样使用执行计划,如查询某个集合的find语句的执行计划
db.collection.<method(...)>.explain()
db.collection.find().explain()

知道了如何使用执行计划,接下来,我们就该来学习执行计划的结果都在"说些什么"。

执行计划的返回结果的详细程度由可选参数verbosity参数决定,通常有三种模式:

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

准备数据:

// 准备数据
db.s4.drop()
db.s4.insertMany([
    {"name": "zhangkai", "age": 18},
    {"name": "wangkai", "age": 28},
    {"name": "likai", "age": 10},
    {"name": "zhaokai", "age": 21}
])

// 为了后续的执行结果信息更全面,这里再创建两个索引 s4_id1 和 s4_id2,两个索引都为age字段创建索引,1表示升序, -1表示降序
db.s4.createIndex({"age": 1}, {"name": "s4_id1"})
db.s4.createIndex({"age": -1}, {"name": "s4_id2"})

接下来,分别使用三种模式查看一条语句的执行计划,观察结果有什么不同。

queryPlanner

queryPlanner是我们最关注的部分,通过这部分信息,我们就能大概的知道语句的执行情况。

// 下面两条执行计划等价
db.s4.explain("queryPlanner").find({"age": {"$gt": 20}})
db.s4.explain().find({"age": {"$gt": 20}})
{
	"queryPlanner" : {
		"plannerVersion" : 1,		// 查询计划版本
		"namespace" : "t1.s4",		// 被查询对象,也就是被查询的集合
		"indexFilterSet" : false,	// 决定了查询优化器对于某一类型的查询将如何使用index,后面会展开来说
		"parsedQuery" : {  // 解析查询,即过滤条件是什么,此处是 age > 20
			"age" : {
				"$gt" : 20
			}
		},
		"winningPlan" : {		// 查询优化器针对该query所返回的最优执行计划的详细内容
			"stage" : "FETCH",  // 最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档
			"inputStage" : {
				"stage" : "IXSCAN",  // 此时的stage是IXSCAN,表示进行的是 index scanning
				"keyPattern" : {
					"age" : 1		// 所扫描的index内容
				},
				"indexName" : "s4_id1",  // 最优计划选择的索引名称
				"isMultiKey" : false,  // 是否是多键索引,因为这里用到的索引是单列索引,所以这里是false
				"multiKeyPaths" : {
					"age" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",  // 此query的查询顺序,forward是升序,降序则是backward
				"indexBounds" : {	// 最优计划所扫描的索引范围
					"age" : [
						"(20.0, inf.0]" // [MinKey,MaxKey],最小从20.0开始到无穷大
					]
				}
			}
		},
		"rejectedPlans" : [ ] // 其他计划,因为不是最优而被查询优化器拒绝(reject),这里为空
	},
	"serverInfo" : {  // 服务器信息,包括主机名和ip,MongoDB的version等信息
		"host" : "cs",
		"port" : 27017,
		"version" : "3.6.12",
		"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
	},
	"ok" : 1
}

executionStats

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

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

db.s4.explain("executionStats").find({"age": {"$gt": 20}})
{
	"queryPlanner" : {
        "plannerVersion" : 1,		// 查询计划版本
		"namespace" : "t1.s4",		// 被查询对象,也就是被查询的集合
		"indexFilterSet" : false,	// 决定了查询优化器对于某一类型的查询将如何使用index,后面会展开来说
		"parsedQuery" : {  // 解析查询,即过滤条件是什么,此处是 age > 20
			"age" : {
				"$gt" : 20
			}
		},
		"winningPlan" : {		// 查询优化器针对该query所返回的最优执行计划的详细内容
			"stage" : "FETCH",  // 最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档
			"inputStage" : {
				"stage" : "IXSCAN",  // 此时的stage是IXSCAN,表示进行的是 index scanning
				"keyPattern" : {
					"age" : 1		// 所扫描的index内容
				},
				"indexName" : "s4_id1",  // 最优计划选择的索引名称
				"isMultiKey" : false,  // 是否是多键索引,因为这里用到的索引是单列索引,所以这里是false
				"multiKeyPaths" : {
					"age" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",  // 此query的查询顺序,forward是升序,降序则是backward
				"indexBounds" : {	// 最优计划所扫描的索引范围
					"age" : [
						"(20.0, inf.0]" // [MinKey,MaxKey],最小从20.0开始到无穷大
					]
				}
			}
		},
		"rejectedPlans" : [ ] // 其他计划,因为不是最优而被查询优化器拒绝(reject),这里为空
    }, 
	"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
			}
		}
	},
	"serverInfo" : {
		"host" : "cs",
		"port" : 27017,
		"version" : "3.6.12",
		"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
	},
	"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的返回

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

allPlansExecution

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

db.s4.explain("allPlansExecution").find({"age": {"$gt": 20}})
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "t1.s4",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"age" : {
				"$gt" : 20
			}
		},
		"winningPlan" : {
			"stage" : "FETCH",
			"inputStage" : {
				"stage" : "IXSCAN",
				"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]"
					]
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	"executionStats" : {
		"executionSuccess" : true,
		"nReturned" : 2,
		"executionTimeMillis" : 0,
		"totalKeysExamined" : 2,
		"totalDocsExamined" : 2,
		"executionStages" : {
			"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" : {
					"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" : [ ]
	},
	"serverInfo" : {
		"host" : "cs",
		"port" : 27017,
		"version" : "3.6.12",
		"gitVersion" : "c2b9acad0248ca06b14ef1640734b5d0595b55f1"
	},
	"ok" : 1
}

IndexFilter

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

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

来个示例:

// 首先,集合 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
}

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

// 通过 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。

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

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

来看应用:

// 对于查询是 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中设定的查询计划,也就是下面这种情况:

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不会影响其他类型的查询,如下面的查询类型,查询优化器还是按照原来的规则选择最优计划:

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

IndexFilter的其他操作:

// 查看指定集合中的IndexFilter数组
db.runCommand({planCacheListFilters: "s4"})

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

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

小结:

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

PS:虽然我着重的介绍了这部分,但执行计划的重点还是queryPlanner和executionStats部分。


that's all,see also:

MongoDB查询性能分析—— explain 操作返回结果详解 | MongoDB执行计划获取(db.collection.explain()) | MongoDB干货系列2-MongoDB执行计划分析详解(1) | MongoDB干货系列2-MongoDB执行计划分析详解(2) | MongoDB干货系列2-MongoDB执行计划分析详解(3) | MongoDB 索引详解 | 如何在MongoDB中使用投影? | MongoDB执行计划获取(db.collection.explain())

posted @ 2021-02-22 11:23  听雨危楼  阅读(2566)  评论(0编辑  收藏  举报