mongdb 查询性能分析及索引优化详解

3,789 阅读5分钟

mongo 性能分析 explain() 方法

MongoDB 查询分析可以确保我们所建立的索引是否有效,是查询语句性能分析的重要工具。
参考文档

可以设置以下参数:

  • queryPlanner
  • executionStats
  • allPlansExecution

常用的是 executionStats 模式

准备工作

我们现在有一张表,一共 5584702 条数据,数据结构和索引状况如下。 单条结构如下:

{
	"_id" : ObjectId("5ff8287c47ec3c1813536e5c"),
	"x" : 1, //递增
	"name" : "MACLEAN",
	"name1" : "MACLEAN",
	"name2" : "MACLEAN",
	"name3" : "MACLEAN"
}

索引状况如下,目前除了 mongodb 默认 _id 索引外,没有添加任何索引:

> db.testdatas.getIndexes()
[
	{
		"v" : 2,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "test.testdatas"
	}
]

开始测试

执行如下查询语句:

> db.testdatas.find({}).limit(10).sort({x: 1}).explain("executionStats")

返回结果

{
	"queryPlanner" : { // 查询计划信息
		"plannerVersion" : 1,
		"namespace" : "test.testdatas", // 该 query 所查询的表
		"indexFilterSet" : false, // 针对该query是否应用索引
		"parsedQuery" : {

		},
		"winningPlan" : { // 最优执行计划的详细内容
			"stage" : "SORT", // 最优执行计划的stage
			"sortPattern" : {
				"x" : 1
			},
			"limitAmount" : 10,
			"inputStage" : { // 子阶段列表
				"stage" : "SORT_KEY_GENERATOR",
				"inputStage" : {
					"stage" : "COLLSCAN",
					"direction" : "forward"
				}
			}
		},
		"rejectedPlans" : [ ] // 其他执行计划
	},
	"executionStats" : { //查询结果执行情况
		"executionSuccess" : true,
		"nReturned" : 10, // 符合查询条件的文档数
		"executionTimeMillis" : 14920, // 选择查询计划和执行查询所需的总时间(以毫秒为单位)
		"totalKeysExamined" : 0, // 索引扫描条目
		"totalDocsExamined" : 5584702, // 扫描文档数
        "executionStages" : {
			"stage" : "SORT",
			"nReturned" : 10,
			"executionTimeMillisEstimate" : 14071,
			"works" : 5584716,
			"advanced" : 10,
			"needTime" : 5584705,
			"needYield" : 0,
			"saveState" : 44054,
			"restoreState" : 44054,
			"isEOF" : 1,
			"invalidates" : 0,
			"sortPattern" : {
				"x" : 1
			},
			"memUsage" : 1160,
			"memLimit" : 33554432,
			"limitAmount" : 10,
			"inputStage" : {
				"stage" : "SORT_KEY_GENERATOR",
				"nReturned" : 5584702,
				"executionTimeMillisEstimate" : 11874,
				"works" : 5584705,
				"advanced" : 5584702,
				"needTime" : 2,
				"needYield" : 0,
				"saveState" : 44054,
				"restoreState" : 44054,
				"isEOF" : 1,
				"invalidates" : 0,
				"inputStage" : {
					"stage" : "COLLSCAN",
					"nReturned" : 5584702,
					"executionTimeMillisEstimate" : 7506,
					"works" : 5584704,
					"advanced" : 5584702,
					"needTime" : 1,
					"needYield" : 0,
					"saveState" : 44054,
					"restoreState" : 44054,
					"isEOF" : 1,
					"invalidates" : 0,
					"direction" : "forward",
					"docsExamined" : 5584702
				}
			}
		}
	},
	"serverInfo" : {
		"host" : "localhost",
		"port" : 27017,
		"version" : "4.0.8",
		"gitVersion" : "9b00696ed75f65e1ebc8d635593bed79b290cfbb"
	},
	"ok" : 1
}

对executionStats返回逐层分析

第一步,分析 executionStats

字段名说明(参考上述查询结果)
executionTimeMillis查询语句总耗时。上述查询语句 5584702 条一共耗时 14920 ms
totalKeysExamined索引扫描条目。 上述结果为 0,说明本次查询可能没有用到索引
totalDocsExamined文档扫描条数。 上述结果为 5584702,说明本次查询把文档全部扫描了一遍
nReturned返回结果条数。 上述结果返回10条

 

第二步,分析 executionStages

这个主要讨论3个返回项,nReturned、totalKeysExamined、totalDocsExamined,

分别代表该条 查询返回的条目、索引扫描条目、文档扫描条目。 这些都是直观地影响到 executionTimeMillis,我们需要扫描的越少速度越快。

对于一个查询,我们最理想的状态是:
nReturned = totalKeysExamined = totalDocsExamined

第三步,分析 stage 状态

字段名说明(参考上述查询结果)
stage查询计划类型。上述结果 SORT, 表明在内存中进行了排序
inputStage用来描述子stage,并且为其父stage提供文档和索引关键字

那么又是什么影响到了totalKeysExamined和totalDocsExamined?是stage的类型。类型列举如下:

COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_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的返回

添加索引后执行同样的查询语句

给该集合添加一个 { x: 1 } 索引

> db.testdatas.createIndex({x: 1})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 1,
	"numIndexesAfter" : 2,
	"ok" : 1
}

执行上文同样的查询语句:

> db.testdatas.find({}).limit(10).sort({x: 1}).explain("executionStats")

返回结果:

{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "test.testdatas",
		"indexFilterSet" : false,
		"parsedQuery" : {

		},
		"winningPlan" : {
			"stage" : "LIMIT",
			"limitAmount" : 10,
			"inputStage" : {
				"stage" : "FETCH",
				"inputStage" : {
					"stage" : "IXSCAN",
					"keyPattern" : {
						"x" : 1
					},
					"indexName" : "x_1",
					"isMultiKey" : false,
					"multiKeyPaths" : {
						"x" : [ ]
					},
					"isUnique" : false,
					"isSparse" : false,
					"isPartial" : false,
					"indexVersion" : 2,
					"direction" : "forward",
					"indexBounds" : {
						"x" : [
							"[MinKey, MaxKey]"
						]
					}
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	"executionStats" : {
		"executionSuccess" : true,
		"nReturned" : 10,
		"executionTimeMillis" : 1,
		"totalKeysExamined" : 10,
		"totalDocsExamined" : 10,
		"executionStages" : {
			"stage" : "LIMIT",
			"nReturned" : 10,
			"executionTimeMillisEstimate" : 0,
			"works" : 11,
			"advanced" : 10,
			"needTime" : 0,
			"needYield" : 0,
			"saveState" : 0,
			"restoreState" : 0,
			"isEOF" : 1,
			"invalidates" : 0,
			"limitAmount" : 10,
			"inputStage" : {
				"stage" : "FETCH",
				"nReturned" : 10,
				"executionTimeMillisEstimate" : 0,
				"works" : 10,
				"advanced" : 10,
				"needTime" : 0,
				"needYield" : 0,
				"saveState" : 0,
				"restoreState" : 0,
				"isEOF" : 0,
				"invalidates" : 0,
				"docsExamined" : 10,
				"alreadyHasObj" : 0,
				"inputStage" : {
					"stage" : "IXSCAN",
					"nReturned" : 10,
					"executionTimeMillisEstimate" : 0,
					"works" : 10,
					"advanced" : 10,
					"needTime" : 0,
					"needYield" : 0,
					"saveState" : 0,
					"restoreState" : 0,
					"isEOF" : 0,
					"invalidates" : 0,
					"keyPattern" : {
						"x" : 1
					},
					"indexName" : "x_1",
					"isMultiKey" : false,
					"multiKeyPaths" : {
						"x" : [ ]
					},
					"isUnique" : false,
					"isSparse" : false,
					"isPartial" : false,
					"indexVersion" : 2,
					"direction" : "forward",
					"indexBounds" : {
						"x" : [
							"[MinKey, MaxKey]"
						]
					},
					"keysExamined" : 10,
					"seeks" : 1,
					"dupsTested" : 0,
					"dupsDropped" : 0,
					"seenInvalidated" : 0
				}
			}
		}
	},
	"serverInfo" : {
		"host" : "localhost",
		"port" : 27017,
		"version" : "4.0.8",
		"gitVersion" : "9b00696ed75f65e1ebc8d635593bed79b290cfbb"
	},
	"ok" : 1
}

加索引后结果分析

第一步,分析 executionStats

executionTimeMillis : 查询总耗时 1ms
totalKeysExamined : 索引扫描条目 10
totalDocsExamined : 文档扫描条目 10

第二步,分析 executionStages

nReturned = 10
totalKeysExamined = 10
totalDocsExamined = 10
目前已达到理想的状态:
nReturned = totalKeysExamined = totalDocsExamined

第三步,分析 stage 状态

从结果看 stage 组合为: Limit+(Fetch+ixscan)

良好的查询组合

对于一般查询,建议stage组合(查询的时候尽可能用上索引):

Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FITER+ixscan
COUNT_SCAN