MongoDB聚合需显式启用explain,推荐用.explain("executionStats");索引是否生效要看winningPlan最底层stage是否为匹配索引的IXSCAN,且避免FETCH回表或COLLSCAN;阶段顺序错位(如$unwind在$match前)会导致索引失效;explain绕过计划缓存,不反映热态行为,也不触发allowDiskUse。

直接用 explain() 方法,但必须加 explain: true 参数或调用 .explain() —— 否则默认不返回执行计划。
aggregate() 调用时必须显式启用 explain
MongoDB 的 db.collection.aggregate() 默认不输出执行计划,和 find().explain() 不同。想看聚合各阶段怎么跑的,得主动告诉它:“我要看计划”。
- 方式一(推荐):在 pipeline 后加
.explain("executionStats"),例如:db.orders.aggregate([{$match: {status: "paid"}}, {$group: {_id: "$region", total: {$sum: "$amount"}}}]).explain("executionStats") - 方式二:用
runCommand并设explain: true:db.runCommand({ aggregate: "orders", pipeline: [{$match: {status: "paid"}}], explain: true }) - 注意:
.explain()不带参数时只返回queryPlanner,看不到实际执行耗时和扫描文档数;要查性能瓶颈,必须用"executionStats"或"allPlansExecution"
explain 输出里哪些字段真正反映索引是否生效
关键不是看有没有 IXSCAN,而是看它出现在哪个阶段、是否被后续操作“废掉”。
-
winningPlan下的最底层 stage 是真实数据来源:如果是IXSCAN且indexName匹配你建的索引,说明 $match 或 $sort 用了索引 - 但如果
IXSCAN上面紧跟着FETCH+PROJECTION,而你又没在索引里包含投影字段(比如$project里要的字段不在索引中),MongoDB 就得回表——这时totalDocsExamined会远大于nReturned - 特别警惕
COLLSCAN出现在winningPlan里:哪怕前面有$match,只要它不在管道最前、或字段没建索引、或被$unwind/$project隔开,就大概率退化为全表扫
聚合管道顺序错位会导致 explain 显示“假索引”
explain 会如实报告它选的计划,但这个计划可能根本没用上你期待的索引——因为阶段顺序让优化器“没法用”。
- $match 和 $sort 必须尽可能靠前,且字段需同时存在于同一个复合索引中;中间插一个
$addFields或$unwind,后面再跟 $match,就无法利用原始集合索引 - 例如:
[{$unwind: "$tags"}, {$match: {"tags.name": "urgent"}}]—— 即使tags.name有索引,explain也可能显示COLLSCAN,因为$unwind产出的是新文档流,索引失效 - 验证方法:把 $match 提到 $unwind 前,再跑一次
.explain("executionStats"),对比executionStages中的stage类型和docsExamined
explain 会绕过查询计划缓存,结果不代表线上真实表现
每次调用 explain() 都强制重新生成候选计划,不读缓存,也不写缓存。这意味着:
- 它反映的是“冷启动”下的最优选择,和你反复执行同一条聚合时的热态行为可能不同
- 如果线上查询慢,但
explain看起来没问题,很可能是缓存了次优计划(比如历史数据分布变化后旧计划变差),此时需要db.collection.deleteIndex()或db.collection.reIndex()触发重规划 -
explain不触发allowDiskUse,即使聚合内存超限,它也不会告诉你“这里会写临时文件”,得靠实际执行 + profiling 日志确认
真正难的不是看懂 explain 字段,而是把 executionStages 树和你的管道顺序、索引定义、数据分布三者对齐——漏掉任意一环,就容易误判“索引已生效”。


















