explain 查看发现,原本使用的高效索引被替换为另一个低效索引。这种执行计划在不同索引之间反复切换、查询延迟波动较大的现象,称为索引跑偏(Plan Drift)。跑偏的关键特征:explain 显示当前使用的索引与历史不同orders 上有两个索引:{ status: 1 }{ uid: 1, createTime: -1 }find({ status, uid }).sort({ createTime: -1 }) 查询形态一致,参数不同时表现差异显著:参数 | status 在全表占比 | 更适合的索引 |
status="cancelled" | 0.1% | { status: 1 }(已取消订单占比较低,按 status 索引扫描成本较小) |
status="paid" | 80% | { uid: 1, createTime: -1 }(按 status 扫描需要遍历大部分数据,按 uid 才能精确定位) |
pending,随业务发展逐步迁移到以 paid 为主mongod 进程重启db.collection.getPlanCache().clear()方式 | 作用范围 | 集群级生效 | 重启后保留 | 适用场景 |
命令上附加 hint | 单条查询 | 否 | 否 | 临时排障 |
应用代码中固定 hint | 业务指定的查询 | 否 | 是(随代码) | 关键业务链路 |
planCacheSetFilter(已废弃) | 同形态全部查询 | 否(每节点单独设置) | 否 | 不推荐 |
setQuerySettings(8.0起推荐) | 同形态全部查询 | 是 | 是 | 推荐方案 |
planCacheSetFilter(Index Filter)已被官方标记为废弃。该命令存在两点固有限制——重启后丢失、需在每个节点单独设置,导致集群环境运维成本较高。官方推荐使用 setQuerySettings 替代,该命令支持集群级生效和重启后保留,适用于副本集与分片集群。hintdb.orders.find({ uid: 1001, status: "paid" }).hint({ uid: 1, createTime: -1 }).sort({ createTime: -1 })
hint# Python 驱动示例cursor = collection.find({"uid": uid, "status": status},hint=[("uid", 1), ("createTime", -1)],).sort([("createTime", -1)])
setQuerySettings(8.0起推荐)setQuerySettings 后,设置会应用于整个集群上的匹配查询结构(适用于副本集和分片集群),且重启后保留:db.adminCommand({setQuerySettings: {find: "orders",filter: { uid: 1, status: "paid" },sort: { createTime: -1 },$db: "shop"},settings: {indexHints: [{ns: { db: "shop", coll: "orders" },allowedIndexes: [ { uid: 1, createTime: -1 } ]}]}})
db.aggregate([{ $querySettings: {} }])
db.adminCommand({removeQuerySettings: {find: "orders",filter: { uid: 1, status: "paid" },sort: { createTime: -1 },$db: "shop"}})
场景 | 推荐方案 |
8.0及以上版本,需集群级、长期持久化的固化 | setQuerySettings(推荐方案) |
关键交易链路、需随代码版本管理 | 应用代码中固定 hint |
临时排障、灰度试验 | 命令上附加 hint |
非关键查询,可接受偶发跑偏 | 不固化,保留自适应能力 |
// Classic 引擎 replan 次数db.serverStatus().metrics.query.planCache.classic.replanned// SBE 引擎 replan 次数db.serverStatus().metrics.query.planCache.sbe.replanned
"replanReason": "cached plan was less efficient than expected: ..."
explain("allPlansExecution") 查看所有候选计划的执行情况,比较 executionTimeMillis、totalKeysExamined、totalDocsExamined,结合具有代表性的业务参数值,确认在主要业务场景下表现稳定的索引。该索引即为固化目标。setQuerySettings 为例:db.adminCommand({setQuerySettings: {find: "orders",filter: { uid: 1, status: "paid" },sort: { createTime: -1 },$db: "shop"},settings: {indexHints: [{ns: { db: "shop", coll: "orders" },allowedIndexes: [ { uid: 1, createTime: -1 } ]}]}})
db.orders.find({ uid: 1001, status: "paid" }).sort({ createTime: -1 }).explain("queryPlanner")
queryPlanner.querySettings:显示已应用的设置内容queryPlanner.winningPlan:使用的索引为目标索引metrics.query.planCache.classic.replanned 与 metrics.query.planCache.sbe.replanned:整体增长趋势应下降,并结合慢查询日志确认该形态查询不再频繁触发 replan// 1. 采集当前所有候选计划的表现,确认新数据下更适合的索引db.orders.find({ uid: 1001, status: "paid" }).sort({ createTime: -1 }).explain("allPlansExecution")// 2. 移除查询设置db.adminCommand({removeQuerySettings: {find: "orders",filter: { uid: 1, status: "paid" },sort: { createTime: -1 },$db: "shop"}})// 3. 清理对应的 Plan Cache,使 MongoDB 基于新数据分布重新评估并选择执行计划db.orders.getPlanCache().clear()
关注点 | 结论 |
跑偏的本质 | Plan Cache 按"查询形态"缓存,但同一形态下不同参数对应的更优索引可能不同 |
高发时段 | 实例重启、新增或删除索引等事件之后的一段时间内 |
发现手段 | 通过 metrics.query.planCache.classic.replanned、metrics.query.planCache.sbe.replanned 以及日志中的 replanReason 进行定位 |
MongoDB 8.0推荐方案 | setQuerySettings:集群级生效、重启后保留、适用于副本集与分片集群 |
实施原则 | 仅对关键业务查询实施固化,非关键查询保留自适应能力 |
planCacheShapeHash 等概念,可用于理解为什么同一形态查询会复用执行计划。queryPlanner、winningPlan、executionStats、totalKeysExamined、totalDocsExamined 等字段,可用于排查慢查询和索引使用情况。explain 和慢查询日志识别查询使用的执行引擎。indexHints、queryFramework 和 reject 等配置项。文档反馈