4. 执行计划全解析
当一条查询变慢时,最忌讳的做法是凭直觉加索引或改语句。MongoDB 提供的 explain() 是唯一能让我们看到"数据库打算怎么执行、实际执行了多少工作"的官方手段。理解它的三种 verbosity 模式以及各模式返回的关键指标,是复杂性能故障诊断的基本功。
三种模式:从"计划"到"真实开销"
explain() 的 verbosity 参数决定返回信息的深度,三者是递进关系,而不是互相替代:
- queryPlanner(默认):只返回查询优化器选出的获胜计划,以及被否决的候选计划概要。它不执行查询,因此不会产生真实执行统计。适合快速确认"优化器选了哪个索引"。
- executionStats:在 queryPlanner 基础上真正执行获胜计划,返回扫描文档数、返回文档数、执行耗时等真实指标。这是日常诊断最常用的模式。
- allPlansExecution:执行获胜计划,同时对候选计划也做有限试跑,返回各候选计划的试跑统计。用于回答"为什么优化器没选我期望的那个索引"。
调用方式非常直接:
db.orders.find({ userId: 1001, status: "PAID" })
.sort({ createdAt: -1 })
.explain("executionStats")
注意:explain() 只描述查询,不修改数据;但 executionStats 与 allPlansExecution 会真实执行查询,对慢查询本身会带来额外负载,生产环境应避免对超大集合频繁调用。
queryPlanner 阶段:优化器在做什么
queryPlanner 返回的核心是 winningPlan 与 rejectedPlans。winningPlan 是一棵计划树,从叶子到根反映数据如何被逐层处理。常见阶段节点包括:
COLLSCAN:全集合扫描,通常意味着没有可用索引或索引未被选中,是性能红灯。IXSCAN:索引扫描,节点内会给出indexName、keyPattern、isMultiKey、direction等。FETCH:根据索引返回的 RecordId 回表取完整文档。SORT:内存排序;若出现SORT_KEY_GENERATOR且无索引支撑,说明排序无法走索引。LIMIT/SKIP/PROJECTION_SIMPLE:结果裁剪阶段。
判读要点:如果 winningPlan.stage 是 COLLSCAN,或计划树里出现 SORT 而排序字段本可被复合索引覆盖,就应优先怀疑索引设计问题。rejectedPlans 则告诉我们优化器考虑过哪些方案,是 allPlansExecution 分析的基础。
executionStats 阶段:真实开销在哪里
executionStats 是诊断的核心。它返回一组直接决定性能好坏的计数:
| 指标 | 含义 | 判读方向 |
|---|---|---|
| nReturned | 最终返回的文档数 | 业务期望值,作为基准 |
| totalKeysExamined | 索引键扫描次数 | 越接近 nReturned 越好 |
| totalDocsExamined | 文档扫描次数 | 远大于 nReturned 说明回表过多 |
| executionTimeMillis | 执行耗时(毫秒) | 结合数据量判断是否异常 |
| executionStages | 各阶段真实统计树 | 定位瓶颈阶段 |
最经典的判读公式是选择性与回表比:理想情况下 totalKeysExamined ≈ totalDocsExamined ≈ nReturned。若 totalDocsExamined 远大于 nReturned,说明索引过滤能力不足,大量文档被回表后才被过滤掉;若 totalKeysExamined 远大于 nReturned,说明索引前缀选择性差,扫描了大量无用键。
例如某订单查询返回 20 条,但 totalKeysExamined = 180000、totalDocsExamined = 175000,这几乎可以断定索引字段顺序不合理——把低基数的 status 放在了复合索引前面,导致优化器只能靠它做粗过滤。
allPlansExecution:为什么没选我想要的索引
当优化器选了一个"看起来更差"的计划时,用 allPlansExecution 可以看到候选计划的试跑结果。它会在 executionStats.allPlansExecution 中列出每个候选计划的 nReturned、executionTimeMillisEstimate 等。常见原因有两类:一是统计信息过期,优化器基于旧基数估算;二是查询形状(如排序方向、范围条件)让某个索引无法被有效利用。此时可结合 hint() 强制指定索引做对比验证。
把指标串成诊断路径
实际排障时,建议按固定顺序读 explain 输出,避免被大量字段淹没:
- 先看
winningPlan.stage:出现COLLSCAN直接定位为索引缺失或失效。 - 再看
executionStats.nReturned与totalDocsExamined的比值:比值远大于 1 说明回表过多。 - 接着看
totalKeysExamined与nReturned:差距大说明索引前缀选择性差。 - 最后看计划树中是否有
SORT:若有且排序字段可被索引覆盖,应调整索引方向或字段顺序。
executionStats 中的 executionTimeMillis 与 nReturned 一起看。若耗时高但返回极少,问题几乎一定在扫描量而非网络或客户端;若返回很多且耗时高,则要区分是索引问题还是结果集本身过大。executionTimeMillis 就下结论。该值受缓存、并发、数据分布影响,同一查询多次执行可能差异明显。判断索引是否有效,应以 totalKeysExamined、totalDocsExamined 与 nReturned 的比例关系为主,耗时作为辅助参考。掌握这三种模式与核心指标的判读逻辑后,面对"查询突然变慢"这类复杂故障,我们就能从"猜"转向"看":先用 queryPlanner 确认计划,再用 executionStats 量化开销,必要时用 allPlansExecution 追问优化器的选择依据,从而把性能问题定位到具体的索引或查询形状上。