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