MongoDB 有一个叫覆盖索引的feature

意思是查询数据的时候,过滤条件和查询字段刚好都是索引,那么MongoDB可能会选择从索引表查询结果,而不需要查询Document。
为什么说是可能的,而不是必须的,这取决于MongoDB的查询计划。
出现问题
最近收到运维的反馈,说某个版本之后开服时间大幅度变长了。这个版本确实是新增了一个数据库表,是从另一个表拆分出来的数据,且在开服的时候需要全表扫描。两个表加起来的数据量在更新前后是一致的。所以预期上不应该出现开服时间大幅度变长的情况。
首先检查了两张表的差别,原来需要扫描的表是有索引字段的,且查询的结果刚好也是索引字段。而新增的是没有索引的。所以怀疑有索引的表,会触发MongoDB的覆盖索引查询,而没有增加的,只能全表扫描。通过对新表增加索引进行对比,发现没有显著的变化。
覆盖索引查询
首先先定位一下,覆盖索引查询 到底有没有被执行。执行db.setProfilingLevel(2);
打开MongoDB的查询分析功能。
1 2 3 4 5
| use [db]; db.setProfilingLevel(2);
|
然后等Skynet这边执行查询操作,查询后在mongo里面执行 db.system.profile.find({"ns" : "[db].[collection]"}).sort({ ts: -1 }).limit(10)
查询某个集合的查询结果。
1 2 3 4 5
| use [db];
db.system.profile.find().sort( { ts : -1 } )
db.system.profile.find({"ns" : "[db].[collection]"}).sort({ ts: -1 }).limit(10);
|
在结果里面查看execStats.inputStage.stage 的结果就可以定位是否使用了覆盖索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "op":"query", "execStats":{ "memLimit":104857600, "type":"simple", "totalDataSizeSorted":0, "usedDisk":false, "inputStage":{ "stage":"COLLSCAN", "nReturned":0, "executionTimeMillisEstimate":0, "works":2, "advanced":0, "needTime":1, "needYield":0, "saveState":0, "restoreState":0, "isEOF":1, "direction":"forward", "docsExamined":0 } } }
|
解决办法
出现这样的问题,是因为在查询数据的时候,过滤条件为空的时候,MongoDB会优先进行全表扫描。当过滤条件不为空,且条件字段刚好是索引的时候,基本上都能触发覆盖索引查询。
针对这种情况,可以使用hint函数,告诉MongoDB,强制使用覆盖索引查询的方式,不在需要进行MongoDB的查询决策了。
下面是未指定hint的查询情况,可以看到 executionStats.executionStages.inputStage.stage的结果是COLLSCAN。未使用覆盖索引查询。
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
| > db.role.find({},{uid:1,'_id':0}).explain("executionStats") { "queryPlanner" : { ... }, "executionStats" : { "executionSuccess" : true, "nReturned" : 7, "executionTimeMillis" : 0, "totalKeysExamined" : 0, "totalDocsExamined" : 7, "executionStages" : { "inputStage" : { "stage" : "COLLSCAN", "nReturned" : 7, "executionTimeMillisEstimate" : 0, "works" : 9, "advanced" : 7, "needTime" : 1, "needYield" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "direction" : "forward", "docsExamined" : 7 } } } }
|
下面是使用hint的情况,可以看到 executionStats.executionStages.inputStage.stage的结果是IXSCAN。未使用覆盖索引查询。查看 totalKeysExamined =7,totalDocsExamined = 0,也可以知道查询的7天数据全部来源于索引表,并未从文档查询过数据。
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
| > db.role.find({},{uid:1,'_id':0}).hint({uid:1}).explain("executionStats") { "queryPlanner" : { ... }, "executionStats" : { "executionSuccess" : true, "nReturned" : 7, "executionTimeMillis" : 0, "totalKeysExamined" : 7, "totalDocsExamined" : 0, "executionStages" : { "stage" : "PROJECTION_COVERED", "inputStage" : { "stage" : "IXSCAN", "nReturned" : 7, "executionTimeMillisEstimate" : 0, "works" : 8, "advanced" : 7, "needTime" : 0, "needYield" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "keyPattern" : { "uid" : 1 }, "indexName" : "uid_1", "isMultiKey" : false, "multiKeyPaths" : { "uid" : [ ] }, "isUnique" : true, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "uid" : [ "[MinKey, MaxKey]" ] }, "keysExamined" : 7, "seeks" : 1, "dupsTested" : 0, "dupsDropped" : 0 } } } }
|
最后
针对一些要查询全表某些字段,而这些字段又刚好是索引的情况。可以使用hint来主动告诉MongoDB使用覆盖索引查询结果。另外因为MongoDB查询数据默认是会返回_id字段的,这个字段也会影响到是否触发覆盖索引查询的情况,可能会即使用了索引查询,也会查询文档的情况。所以如果这个字段不需要使用,是需要去除的。
skynet比较旧的版本是不支持hint操作的。这个需要修改skynet源码或者升级skynet版本来支持。因为后面的版本skynet使用的是op_msg协议接入MongoDB了。而我们MongoDB版本还不支持,所以只能修改skynet源码才能实现。