explain("executionStats")是调优第一动作,通过totalDocsExamined与nReturned比值判断索引是否生效,两者接近但耗时高则问题在$unwind或$group等管道阶段,memUsage超限或COLLSCAN出现需立即优化。
聚合慢,八成不是写法错,而是索引没生效或阶段顺序反了。 直接上手调优比猜原因快得多——先跑 explain("executionStats"),再按数据扫描量和内存占用动刀。
别等用户投诉才查,每次改完聚合管道都该跑一遍。它不只告诉你耗时,更暴露真实执行路径:
totalDocsExamined 远大于 nReturned(比如 50 万 vs 200)→ 过滤太晚或索引没命中,立刻检查第一个 $match 字段有没有建索引totalKeysExamined 接近 totalDocsExamined 但耗时高 → 索引生效了,问题在管道阶段本身,比如 $unwind 后文档爆炸、$group 前没 $project 精简字段stage 下出现 "memUsage": 98765432 → 内存超限风险,该加 allowDiskUse: true 了,否则会静默降级变慢stage: "COLLSCAN" 出现在 $lookup 的 from 集合里 → 关联集合缺索引,不是主集合的问题索引只对聚合管道中「第一个能走索引的 $match」生效。后面再写 $match,数据已经变形,索引基本失效。
$match 中的查询顺序:db.logs.createIndex({userId: 1, createdAt: 1}) 能加速 {$match: {userId: "u1", createdAt: {$gte: ...}}},但对 {$match: {createdAt: ..., userId: "u1"}} 效果差甚至不用ObjectId,查询却传字符串 ID,索引直接跳过$lookup 内部的 $match(pipeline 形式)也得单独建索引,而且得建在 from 集合上,不是主集合不是“内存不够才开”,而是“只要管道里有 $group、$sort 或 $unwind,且输入数据量不可控,就该默认打开”。
$group 会自动降级到磁盘,但不开 allowDiskUse 会直接报错 errmsg: "Exceeded memory limit..."
explain 看是否真触发了磁盘落盘{allowDiskUse: true},别依赖静默行为很多人以为 $project 就是选几个字段返回,其实它直接影响中间数据体积和内存压力。
$match 后立刻 $project,只保留后续阶段真正需要的字段(比如 $group 只用 userId 和 amount,那就只投这两个)$project 里做复杂计算(如 $concat、$dateToString),这些操作在每条文档上执行,拖慢整个阶段_id: 0 别省——尤其当 $group 后接 $sort,多余字段会一起排序,徒增开销真正卡住性能的,往往不是某一行代码,而是 $match 没走索引、$unwind 后没 $project、或者 allowDiskUse 没开却指望大数据量稳如老狗。这些点不盯死,调参、换硬件都没用。