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

直接看 explain("executionStats"),别猜——它告诉你真实耗在哪
为什么 explain("executionStats") 是调优第一动作
聚合慢不等于写法错,可能是索引没生效、阶段顺序导致数据膨胀、或内存溢出被静默降级。只靠执行时间(比如 2.4s)无法定位根因。explain("executionStats") 返回的 executionStats 字段里藏着三个关键数字:totalDocsExamined(扫描文档数)、totalKeysExamined(索引键扫描数)、executionTimeMillis(实际耗时)。三者关系一目了然:如果 totalDocsExamined 远大于 totalKeysExamined,说明索引没被用上;如果两者接近但耗时仍高,问题可能在管道阶段本身(如 $unwind 后文档爆炸)。
explain 返回里最该盯住的 3 个字段
-
totalDocsExamined:MongoDB 实际从磁盘/内存读取的原始文档数量。理想情况应 ≈ 最终输出文档数,若达百万级而输出仅百条,说明过滤太晚或索引失效 -
nReturned:最终返回给客户端的文档数。和totalDocsExamined的比值就是“过滤效率”,低于 1% 就该查$match位置 -
memUsage(出现在executionStats的stages每层中):某个阶段内存占用。若某阶段出现"memUsage": 98765432(超 90MB),且后续有$group或$sort,大概率触发allowDiskUse但你没显式开——此时会静默变慢且不稳定
用 explain 发现典型陷阱的实操路径
执行带 explain("executionStats") 的聚合后,按以下顺序快速扫一眼:
- 先看顶层
executionStats.executionTimeMillis和totalDocsExamined—— 若后者是前者的千倍以上,立刻检查第一个$match是否用了索引字段 - 展开
executionStats.stages数组,找stage: "IXSCAN"是否出现在第一个非$cursor阶段;没有?说明$match没走索引,或索引字段顺序/类型不匹配 - 找
stage: "COLLSCAN"—— 出现在任何位置都危险,尤其在$lookup的 from 集合上,意味着关联集合缺索引 - 查
stage: "GROUP"下的memUsage和docsExamined:如果docsExamined是输入的 10 倍以上,大概率前面$unwind没配$project精简,导致中间文档量失控
容易被忽略的细节:explain 不等于真实执行
explain("executionStats") 会真实执行管道,但部分副作用被跳过(如 $out、$merge 不写库,$facet 可能不完整计算)。更关键的是:它不会触发 allowDiskUse 的磁盘落盘行为——也就是说,如果原聚合因内存超限而启用磁盘暂存,explain 里看到的 memUsage 可能远低于实际运行时峰值,导致误判。真要压测内存,得关掉 explain,加 { allowDiskUse: true } 并监控 mongod 进程 RSS 内存。











