聚合查询卡住主因是未前置$match和缺失索引,导致$sort/$group全量加载文档抢锁占内存;必须将高选择性$match置首并建匹配复合索引,禁用allowdiskuse而用explain定位collscan,辅以maxtimems和$limit防阻塞。

聚合查询为什么会让线上业务卡住
不是因为聚合本身慢,而是它默认会抢锁、占内存、拖垮整个连接池。尤其当 $sort 或 $group 阶段没走索引,MongoDB 就得把几万甚至几十万文档全拉进内存排序——一旦超 32MB(internalQueryExecMaxBlockingSortBytes 默认值),就会写磁盘,同时阻塞其他请求排队等待。
更隐蔽的是:聚合里带 $lookup 或多阶段管道时,WiredTiger 引擎可能长时间持有 collection 级锁,导致后续的 updateOne、insert 全部 hang 在 waitingForLock 状态。你在 db.currentOp() 里看到大量操作 secs_running > 30 且 type: "op",基本就是它在作祟。
必须前置的 $match 和索引设计
聚合管道里唯一能利用索引的阶段,只有最开头的 $match。它后面所有阶段($sort、$group、$unwind)都只能处理上一阶段输出的数据,无法回退触发索引。
- 把过滤条件尽可能提前:比如查“2026年6月订单”,别写成先
$unwind再$match,而要$match放第一行,用{ order_date: { $gte: ..., $lt: ... } } - 复合索引字段顺序必须匹配
$match查询模式:如果管道开头是{ status: "paid", user_id: ObjectId("...") },索引就得建为{ status: 1, user_id: 1 },反过来就失效 - 避免在
$match里用不支持索引的操作符:$regex开头不是^、$where、$text(除非配了全文索引)都会退化为 COLLSCAN
别乱开 allowDiskUse:true,先看 explain
加 allowDiskUse: true 不是优化,是妥协。它让聚合落盘继续跑,但 I/O 暴增会直接拖慢整个节点的响应——尤其在机械盘或低 IOPS 的 SSD 上,线上业务读写请求全部排队。
真正该做的是用 explain("executionStats") 查清瓶颈:
- 看
totalDocsExamined是否远大于nReturned:如果是,说明$match没走索引,赶紧补 - 查
planSummary是否出现COLLSCAN:出现就等于没索引,别调参,先建索引 - 盯
spilledDataStorage字段:非空说明已写磁盘,此时再开allowDiskUse只是雪上加霜
顺便提醒:explain() 本身会真实执行一次查询,别在高峰期对大集合直接跑——先切到从节点或低峰期验证。
高并发场景下聚合必须加超时和资源限制
MongoDB 不会自动给聚合设 timeout,一个慢聚合能卡住整个连接,连带拖垮 mongos 或应用层连接池。必须显式控制:
- 用
maxTimeMS限执行时间:db.collection.aggregate([...], { maxTimeMS: 3000 }),超时直接报ExceededTimeLimit,不阻塞后续请求 - 对长耗时聚合单独路由:在 mongos 层配置 readPreference=secondaryPreferred,让它跑在从节点,主节点专注写入
- 业务侧做降级:比如报表类聚合失败时,返回缓存的昨日数据,而不是让用户干等
最容易被忽略的一点:聚合结果集大小没限制。如果 $group 输出几万条文档,driver 会一次性全 load 进内存反序列化——应用服务 OOM 就发生在这一步。务必用 $limit 控制输出条数,或分页查($skip+$limit,但注意性能代价)。











