explain("executionStats")是调优第一动作,它通过totalDocsExamined、totalKeysExamined和executionTimeMillis三者关系定位根因:前者远大于后者说明索引未生效,两者接近但耗时高则问题在管道阶段如$unwind膨胀。

用 explain("executionStats") 看真实执行耗时,别信聚合返回时间
聚合命令返回的耗时(比如 shell 里显示的 "elapsedMillis")只含网络往返和锁等待,不是管道内部各 stage 的真实计算时间。真正能拆解每个阶段花了多久的,只有 explain("executionStats") ——它会实际执行一次管道,并在 executionStats.stages 数组里逐层给出 executionTimeMillis。
注意:它会真实读数据、走索引、触发内存统计,但跳过写操作(如 $out、$merge),所以结果可信,但别在高峰期对大集合反复跑。
- 必须显式传
"executionStats",只写explain()或explain("queryPlanner")不会返回阶段级耗时 - 返回结构里
executionStats.executionTimeMillis是总耗时,而executionStats.stages是数组,每个元素对应一个 stage,带自己的executionTimeMillis - 如果某 stage 的
executionTimeMillis占比超 70%,且它的docsExamined很小,大概率是上游输出了大量冗余字段(比如没$project就进了$group)
$sort 和 $group 阶段的 executionTimeMillis 常被高估,因为它们包含等待时间
$sort 和 $group 是阻塞式阶段,必须等上游所有文档到达才开始处理。所以它们的 executionTimeMillis 实际包含两部分:上游数据“排队”时间 + 真正排序/分组时间。这会导致数值虚高,尤其当上游没过滤或没投影时。
- 例如:上游
$match没命中索引,扫描了 50 万文档,那$sort的executionTimeMillis里至少有几十毫秒是在等这 50 万条“慢慢流进来” - 看
executionStats.stages[n].stage === "SORT"时,顺手检查它的memUsage—— 若接近 100MB,说明已逼近 WiredTiger 默认内存限制(104857600 字节),此时即使开了allowDiskUse: true,也可能因中间文档过大被拒绝落盘 - 不要单独优化
$sort耗时,先确认它的输入量是否合理;查stages[n-1].nReturned,如果远大于最终nReturned,问题不在$sort本身
用 system.profile 补充定位线上慢聚合的真实瓶颈点
explain("executionStats") 是单次调试利器,但它不反映并发争抢、锁等待、网络抖动等线上真实压力。这时得靠 system.profile——它记录的是端到端真实延迟,含锁、排队、IO 等全部开销。
- 启用方式:
db.setProfilingLevel(1, {slowms: 200})(生产环境建议 ≥200ms,避免日志刷爆) - 查慢聚合:
db.system.profile.find({"command.aggregate": {$exists: true}, "millis": {$gt: 200}}).sort({"ts": -1}).limit(3) - 重点看返回里的
command.aggregate数组顺序,再结合millis和nscannedObjects判断:如果第 2 个 stage 耗时占总millis的 90%,但nscannedObjects很小,说明前一阶段输出膨胀严重(比如$unwind后没$project就直接$group)
别忽略 explain 里 memUsage 和 docsExamined 的交叉线索
单看某个 stage 的 executionTimeMillis 容易误判。真正要交叉验证的是:它的 memUsage 是否异常高?它的 docsExamined 是否远大于上一阶段的 nReturned?这两个数字一并看,才能区分是“真慢”还是“假慢”。
- 例如:
$group阶段executionTimeMillis是 800ms,memUsage是 92MB,但docsExamined是 1200 —— 这说明它处理的文档不多,但每条都很“胖”,大概率是上游没$project,把整个原始文档(含大数组、长字符串)全传进来了 - 又例如:
$lookup阶段executionTimeMillis高,但docsExamined接近 0,from集合却出现COLLSCAN—— 那就是关联集合缺索引,不是主集合的问题 - 最危险的情况:
$sort或$group的memUsage接近 100MB,且allowDiskUse: true没开 —— 此时 MongoDB 会静默降级为内存内排序,性能断崖式下跌,但错误日志里可能只报一句模糊的"Sort exceeded memory limit..."


















