聚合查询CPU飙升主因是$match未前置或缺失索引,导致全表扫描;应确保$match在管道最前、字段已建符合ESR规则的索引,并配合.lean()和$project减少内存开销。

聚合查询没加索引或阶段顺序错,是导致MongoDB CPU飙升最常见原因——不是代码写得不够“高级”,而是数据还没筛干净就进管道了。
为什么$match必须放在聚合管道最前面?
因为MongoDB只在$match、$sort、$limit这几个阶段能直接利用索引。一旦前面有$lookup、$unwind或$group,后续的$match就只能在内存里扫文档,docsExamined会指数级上升。
常见错误现象:explain("executionStats")里看到totalDocsExamined远大于totalKeysExamined,比如前者是50万、后者是0——说明全表扫描了。
- 确保
$match用的字段已建索引,且复合索引顺序匹配ESR规则(等值→排序→范围) - 避免在
$match里混用$ne、$not、未左锚定的正则,这些基本会让索引失效 - 如果业务允许,把时间范围、状态码这类高选择性条件前置,哪怕只是先筛掉80%数据
如何用lean()和project配合降低内存压力?
Mongoose默认返回的是带方法和校验逻辑的Document实例,但聚合结果根本不需要这些。不关掉它,每条文档都会多占几KB内存,CPU还得做额外序列化/反序列化。
使用场景:你只要原始数据做报表、导出或前端渲染,不调.save()、.validate()之类的方法。
- 在Mongoose Model上调用
aggregate()后,链式调用.lean(),返回纯JS对象而非Document - 再配合
$project显式指定字段,比如{$project: {name: 1, score: 1, _id: 0}},避免传输几十个用不到的字段 - 注意:
.lean()后不能用.populate(),需要关联数据得靠$lookup在管道里完成
什么时候必须加allowDiskUse?
当聚合中间结果超过100MB,默认会报错errmsg: "Sort exceeded memory limit"。强行加大内存不是办法,MongoDB会把溢出部分写到磁盘临时文件——这本身不慢,但CPU要参与序列化、压缩、IO调度,负载反而更高。
关键判断点:看explain("executionStats")里的memoryUsage和spilledDataStorage字段。
- 只要
spilledDataStorage> 0,就得加{ allowDiskUse: true },否则查询直接失败 - 但更该做的是:检查是不是
$group或$sort前没过滤够——比如按用户ID分组前,没用$match限定最近7天数据 - 在分片集群里,
allowDiskUse需在每个shard上生效,不是只配一次就完事
explain()里哪些字段真正反映CPU负担?
别只盯着executionTimeMillis。CPU高往往是因为单次查询干了太多事,而这个“事”的数量藏在底层统计里。
-
totalKeysExamined:越低越好。接近0说明索引完全没用上 -
totalDocsExamined:和上面数字差距越大,说明索引效率越差;如果它比集合总文档数还大,可能是$unwind后膨胀了 -
nReturned:最终返回多少条。如果它只有10条,但totalDocsExamined是10万,那99%的CPU时间都花在白扫上了 -
executionStages.stage里出现COLLSCAN,就是铁证——立刻查索引和$match位置
复杂点在于:同一个聚合管道,在不同数据分布下可能走不同执行计划。今天explain看着好,明天数据倾斜了,CPU就突然拉满。所以得定期用真实时间段+真实参数跑explain,而不是只测空库或固定ID。











