聚合查询执行慢必须先用explain("executionStats")确认是否走索引:若totalDocsExamined远大于nReturned或winningPlan.stage为COLLSCAN,说明未有效利用索引;需确保$match置于管道首位、字段顺序与复合索引严格一致,并避免$lookup+$unwind引发数据膨胀。

聚合查询执行慢,先看执行计划是否走索引
不查 explain() 就调优,等于蒙眼修车。必须先确认 MongoDB 是否真的用了索引,而不是全表扫描(COLLSCAN)。
运行带 explain("executionStats") 的聚合命令,重点看两个字段:
-
totalDocsExamined:如果远大于nReturned,说明大量文档被读取但未命中条件 -
totalKeysExamined:应接近或等于totalDocsExamined,否则索引没被有效利用 - 如果
winningPlan.stage === "COLLSCAN",那当前查询压根没走索引
常见误操作:只对单个字段建了索引,但聚合里 $match 同时用了 status 和 createTime,却没建复合索引。
$match 阶段必须放在管道最前面,且字段要匹配索引顺序
聚合管道不是“先算再筛”,而是“边流边处理”。越早过滤,后续阶段处理的数据量就越小。把 $match 放在 $group 或 $sort 后面,性能会断崖式下降。
更关键的是:索引字段顺序必须和 $match 中的查询条件顺序一致。例如:
你建了 { userId: 1, createTime: -1 } 的复合索引,但聚合里写的是:
{$match: { createTime: { $gte: ISODate("2025-01-01") }, userId: "U123" }}
这个 $match 无法命中索引——因为 createTime 是范围查询,而它在索引里是第二位,MongoDB 只能用上 userId 部分,createTime 仍要扫描。
正确写法必须是:
{$match: { userId: "U123", createTime: { $gte: ISODate("2025-01-01") } }}
避免 $lookup + $unwind 导致数据爆炸
$lookup 关联后如果目标集合匹配多条记录,再跟 $unwind,文档数量可能指数级膨胀。比如一个订单关联 5 个商品,10 万订单就变成 50 万文档进入后续阶段——内存爆、排序慢、甚至触发 Exceeded memory limit 错误。
缓解方式有三:
- 在
$lookup的pipeline参数里加$match,提前过滤掉不需要的关联项 - 用
$addFields+$size判断数组长度,必要时用$limit: 1控制展开数量 - 如果只是要统计关联数(如“每个用户下单多少次”),优先用
$lookup+$size,而不是$unwind+$group
内存超限($group / $sort)时,必须开 allowDiskUse
MongoDB 默认限制聚合内存使用 100MB。一旦 $group 或 $sort 阶段数据量过大,就会报错:Exceeded memory limit for $group, but didn't allow disk use。
这不是调优终点,而是起点——开了 allowDiskUse: true 只是让查询不崩,但磁盘临时文件 IO 会让速度更慢。
真正该做的是:
- 确认
$group的_id字段是否过度细分(比如用完整时间戳+毫秒级字段当分组键) - 用
$project提前裁剪无关字段,减少每条文档体积 - 若分组基数实在太大,考虑拆成两阶段聚合:先按粗粒度分组汇总,再用应用层或二次聚合细化
复杂点在于:索引优化、阶段顺序、内存控制这三者必须协同调整,单独改某一项往往效果有限。最容易被忽略的是 —— $match 条件字段顺序和复合索引字段顺序是否严格对齐。

















