一、接着上文
上文已说到totalIds在or查询条件中,所以不会匹配到索引。
本文我们试着调整下查询条件,观察调整后,特别是totalIds字段,将匹配哪个索引。(观察的依据仍是查询计划的executionStats)
- 把totalIds从or查询条件中提取出来
// 调整前
"$or": [{ "auth": 1 },{ "totalIds": { "$in": [10001] } }
]// 调整后
{ "totalIds": { "$in": [10001] } }
实际业务调整,让请求客户端,选择两者中的任意一个条件。这样,就需要写or条件了。
至于auth查询条件,想要查询变快,可以创建索引auth_1_createdOn_0,另外缩短查询的时间范围。(我们近一年auth=1的数据量不过2000条)
本文的主要内容是针对totalIds字段如何匹配索引,以及使用哪个组合索引。
二、新的执行计划
执行的步骤是(由内往外):IXSCAN -> FETCH -> OR -> FETCH -> SORT_KEY_GENERATOR -> SORT -> PROJECTION
可以看到,查询耗时提高了至少百倍,只需要37毫秒,检索的索引数量由十几万减少到几百。
'executionStats': {'nReturned': 4,'executionTimeMillis': 37,'totalKeysExamined': 486,'totalDocsExamined': 486,'executionStages': {'stage': "SINGLE_SHARD",'nReturned': 4,'executionTimeMillis': 37,'totalKeysExamined': 486,'totalDocsExamined': 486,'totalChildMillis': NumberLong("37"),'shards': [{'shardName': "d-bp1cef3c8241a8a4",'executionSuccess': true,'executionStages': {'stage': "PROJECTION",'nReturned': 4,'executionTimeMillisEstimate': 1,'works': 495,'advanced': 4,'needTime': 489,'needYield': 0,'saveState': 34,'restoreState': 34,'isEOF': 1,'invalidates': 0,'transformBy': {'$sortKey': {'$meta': "sortKey"}},'inputStage': {'stage': "SORT",'nReturned': 4,'executionTimeMillisEstimate': 1,// 略'inputStage': {'stage': "SORT_KEY_GENERATOR",'nReturned': 4,'executionTimeMillisEstimate': 1,// 略'inputStage': {'stage': "FETCH",'filter': {// 略},'nReturned': 4,'executionTimeMillisEstimate': 1,// 略'inputStage': {'stage': "OR",'nReturned': 486,'executionTimeMillisEstimate': 0,// 略'inputStages': [{'stage': "FETCH",'filter': {'recycle': {'$eq': null}},'nReturned': 0,'executionTimeMillisEstimate': 0,// 略'inputStage': {'stage': "IXSCAN",'nReturned': 0,'executionTimeMillisEstimate': 0,// 略}},{'stage': "IXSCAN",'nReturned': 486,'executionTimeMillisEstimate': 0,// 略}]}}}}}}]}
},
- IXSCAN
查询语句匹配到了索引totalIds_1_isDelete_1_recycle_1_creatorName_1,其中 ‘isMultiKey’: true, 因为totalIds是一个数组。
'stage': "IXSCAN",
'nReturned': 486,
'executionTimeMillisEstimate': 0,
'works': 487,
'advanced': 486,
'needTime': 0,
'needYield': 0,
'saveState': 34,
'restoreState': 34,
'isEOF': 1,
'invalidates': 0,
'keyPattern': {'totalIds': 1.0,'isDelete': 1.0,'recycle': 1.0,'creatorName': 1.0
},
'indexName': "totalIds_1_isDelete_1_recycle_1_creatorName_1",
'isMultiKey': true,
'multiKeyPaths': {'totalIds': ["totalIds"],'isDelete': [],'recycle': [],'creatorName': []
},
'isUnique': false,
'isSparse': false,
'isPartial': false,
'indexVersion': 2,
'direction': "forward",
'indexBounds': {'totalIds': ["[10001, 10001]"],'isDelete': ["[false, false]"],'recycle': ["[0, 0]"],'creatorName': ["[MinKey, MaxKey]"]
},
'keysExamined': 486,
'seeks': 1,
'dupsTested': 486,
'dupsDropped': 0,
'seenInvalidated': 0
OR
“第一个”FETCH和IXSCAN两个stage之间是OR关系。这里的IXSCAN和上一个stage相同。
这里说它是“第一个”FETCH,因为外层还有一个FETCH。
本阶段主要是针对recycle查询条件,这也提醒我们在编程的时候,对字段不要赋空,要赋一个默认状态的值。
就拿本例的recycle字段为例:
- 保存记录的时候,recycle=0
- 放入回收站的时候,recycle=1
- 从回收站还原的时候,recycle=0
如此,recycle字段的值要么是0,要么是1。
而查询条件也就可以优化,不存在or查询了。
// 修改前
{"$or":[{"recycle":null},{"recycle":0}]}// 修改后
{"recycle":0}
“第二个”FETCH
和前文不同,根据创建时间区间检索,由IXSCAN阶段改为FETCH阶段了。
'stage': "FETCH",
'filter': {'$and': [{'createdOn': {'$lte': ISODate("2024-04-29T00:00:00.000Z")}},{'createdOn': {'$gte': ISODate("2023-04-28T00:00:00.000Z")}},{'classroomName': {'$regex': ".*大口加小口.*"}}]
},
SORT
SORT排序是在内存中进行排序,所以它的速度非常快。
三、组合索引的差异
前文,我们说了,包含totalIds字段的组合索引有两个:
- totalIds_1_isDelete_1_recycle_1_creatorName_1
- totalIds_1_createdOn_2
mongodb匹配的是前者,而非后者。(个人认为,Mongo应该是根据组合索引匹配的字段数量来,前者匹配了三个字段,而后者是两个字段)
这也给我们建组合索引提了个醒,不要像我们这样,一通下来,期望匹配的索引(totalIds_1_createdOn_2)反而落榜了。
如果你想要指定使用索引totalIds_1_createdOn_2,在末尾指定hint(“totalIds_1_createdOn_2”)
见下所示:
当然,对于本查询来说,只要查询条件totalIds匹配了索引,由于其区分度非常高,最后的查询效率都很高。(查询只需几十毫秒)
下面说一说,当指定使用索引totalIds_1_createdOn_2时,执行计划变成怎么样了。
执行步骤为(由内往外):IXSCAN --> FETCH --> SORT_KEY_GENERATOR --> SORT --> PROJECTION
"winningPlan" : {"stage" : "PROJECTION","transformBy" : {"$sortKey" : {"$meta" : "sortKey"}},"inputStage" : {"stage" : "SORT","sortPattern" : {"createdOn" : -1},"limitAmount" : 20,"inputStage" : {"stage" : "SORT_KEY_GENERATOR","inputStage" : {"stage" : "FETCH","filter" : {// 略},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"totalIds" : 1,"createdOn" : 1},"indexName" : "totalIds_1_createdOn_2","isMultiKey" : true,"multiKeyPaths" : {"totalIds" : ["totalIds"],"createdOn" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"totalIds" : ["[MinKey, MaxKey]"],"createdOn" : ["[MinKey, MaxKey]"]}}}}}}
最内层的索引数据检索,使用了我们指定的索引totalIds_1_createdOn_2。
四、总结
通过两篇文章,我们举例一个集合的多个组合索引,对比分析其执行计划。
因为只有执行计划,才是判定你的查询语句最后是否使用了索引,以及哪个索引。
最后,当一个字段被包含在多个组合索引的时候,更加要小心了,因为我们程序是不会指定使用哪个索引。