本文将为您介绍什么是查询计划,以及出现查询重规划(Replan)的原因和处理方法。
查询计划器(Query Planner)
MongoDB查询计划器能够根据可用的索引给每个查询选择并缓存最有效的查询计划,查询计划器工作流程如下图。
查询计划器会根据候选计划时查询执行的工作单元(works)数量评估出最有效的查询计划,成功缓存的查询计划条目后续可以用于具有相同查询样式的查询。
查询计划缓存中的条目包含了以下3种状态:
缺失:不存在于查询计划缓存中。
非活跃:存在于查询计划缓存中,并且经过评估生成了works值,可转换成活跃状态。
活跃:存在于查询计划缓存中,曾经胜出的计划,可转换为非活跃状态。
查询计划缓存是完全存储在内存中的,并不会持久化,MongoDB每次重启时缓存都会清空。此外,删除表和删除索引也会清理查询计划缓存。查询计划缓存数量存在上限,遵循LRU算法的缓存替代策略,因此会不定期淘汰不常命中的缓存条目。
特殊情况下,您可以执行如下命令管理查询计划:
清空指定集合的查询计划缓存。
db.<collection>.getPlanCache().clear()
查看指定集合下所有查询模式。
db.<collection>.getPlanCache().listQueryShapes()
查看指定查询的查询计划。
db.<collection>.getPlanCache().getPlansByQuery({"query": {"name": "testname"}, "sort": { "name": 1 })
查询哈希以及查询计划缓存键
MongoDB从4.2版本开始,为了定义查询模式(Query Shape),新增了查询哈希(queryHash),并为每一种不同的查询模式生成了对应的查询哈希。与查询哈希不同,查询计划缓存键(planCacheKey)是查询模式和该查询模式当前可用索引的函数。如果添加或删除了查询模式的索引,查询计划缓存键可能会更改,而查询哈希值不会更改。
例如,一个表存在如下索引和查询模式:
索引
db.foo.createIndex( { x: 1 } ) db.foo.createIndex( { x: 1, y: 1 } ) db.foo.createIndex( { x: 1, z: 1 }, { partialFilterExpression: { x: { $gt: 10 } } } )
查询模式
db.foo.explain().find( { x: { $gt: 5 } } ) // 查询操作1 db.foo.explain().find( { x: { $gt: 20 } } ) // 查询操作2
第三个索引仅能支持查询操作2而无法支持查询操作1,因此这两个查询操作具有完全不同的查询计划缓存键。当新增加一个{x:1, a:1}
的索引时,这两个查询计划缓存键也会更新。
查询重规划(replan)
当集合中的数据发生变化时,已缓存的计划可能不再是最佳选择。由于数据是动态变化的,因此查询计划也要随之变化。
当您运行与缓存计划具有相同查询模式的查询时,查询计划器不会计算该查询计划,查询计划器将直接使用缓存里的计划来执行查询。与此同时,查询计划器也将持续评估该计划的执行效率,如果查询计划器确定现在使用缓存计划的效率比另一个查询计划低10倍以上,那么将停止执行,从缓存中逐出计划,并重新开始评估查询计划。上述过程就是查询重规划。
replan的影响以及解决方法
您可能会在慢日志中看到关键字"replanned":true
,这表示数据库无法针对特定查询模式的查询条件提供始终有效的计划。
慢日志示例如下。
"replanned":true,"replanReason":"cached plan was less efficient than expected: expected trial execution to take X works but it took at least 10X works"
影响
频繁地发生replan可能会影响查询性能。
大量的Query Replanning可能出现争抢互斥锁,导致CPU使用率过高。
解决方法
临时升级实例规格以缓解数据库负载压力,具体操作,请参见变更配置。
尝试清理查询计划,再观察查询分析器能否选择到更优的计划。
在业务端对发生replan的查询条件使用
hint()
来指定索引,示例如下。db.<collection>.find({a:"ABC"},{b:1,_id:0}).sort({c:1}).hint({ a:1, c:1, b:1} )
说明Hint会覆盖查询计划器正常选择查询计划行为。
在服务端对发生replan的查询条件使用索引过滤来限制使用的索引,示例如下。
// 设置索引过滤 db.runCommand( { planCacheSetFilter: "<collection>", query: { a: "ABC" }, projection: { b: 1, _id: 0 }, sort: { c: 1 }, indexes: [ { a: 1, c: 1 , b: 1 } ] } ) // 移除之前的设置 db.runCommand( { planCacheClearFilters: "<collection>" } )
说明索引过滤器会覆盖查询计划器正常选择查询计划的行为。
当一个查询同时存在Hint和索引过滤器时,索引过滤器会覆盖指定的Hint值,因此,您应当谨慎使用索引过滤器。更多介绍,请参见Index Filters。
【推荐】优化查询并为它们创建有效的索引,以避免查询重新规划。
说明Hint和索引过滤器的方式并非最佳解决方案,在大多数情况下,检查和修改查询语句、可用索引和文档模式会产生更好的结果。
【推荐】如果您的MongoDB实例大版本为4.2或4.4,建议将实例内核小版本升级至最新版,可以有效减少互斥锁的使用。您也可以选择将实例升级至5.0或6.0大版本来解决上述问题。关联的内核JIRA ticket的说明,请参见SERVER-40805。升级数据库小版本和大版本的方法,请参见升级数据库小版本和升级数据库大版本。
如果上述方法都没有产生效果,您可以提交工单联系技术支持协助解决。