4.2 聚合优化与慢聚合事故


4.2 聚合优化与慢聚合事故

本节摘要:聚合优化的三板斧是尽早过滤、尽早限制、让 match 与 sort 吃到索引。本节从一次大屏接口 40 秒超时的事故讲慢聚合的定位方法与改写手法。

事故档案 09:40 秒的实时大屏

运营大屏每 10 秒拉一次"各城市今日 GMV"。管道先 unwind 订单商品数组,再 match 当天,最后 group。早高峰后接口耗时升到 40 秒,前端雪崩。explain 复盘:match 排在 $unwind 之后,过滤发生在文档被放大十倍之后。

// 事故版:unwind 放大后再过滤 [ { $unwind: "$items" }, { $match: { createdAt: { $gte: today } } }, { $group: { _id: "$city", gmv: { $sum: "$items.amount" } } } ] // 修复版:过滤提到放大之前,且 createdAt 有索引 [ { $match: { createdAt: { $gte: today } } }, // 吃索引,扫描量降两个数量级 { $unwind: "$items" }, { $group: { _id: "$city", gmv: { $sum: "$items.amount" } } } ]

优化清单

  1. match/limit 前置:数据越早变少,后面每个阶段都省;
  2. 索引利用:管道开头的 match + sort 若命中复合索引,可跳过全扫与大内存排序;
  3. $project 减列:只留聚合需要的字段,减小中间文档;
  4. $lookup 约束:局部集合小、外键有索引,否则一次 join 等于 N 次查询;
  5. 预聚合:高频大屏类统计别每次现算,用 $merge 落成结果集合定时刷新。
// 用 $merge 做预聚合:每小时把结果写进汇总集合 db.orders.aggregate([ { $match: { hour: "2026-08-22-10" } }, { $group: { _id: "$city", gmv: { $sum: "$amount" } } }, { $merge: { into: "gmv_hourly", on: "_id", whenMatched: "replace" } } ]); // 大屏只读 gmv_hourly,毫秒级

聚合耗时拆解

⚠️ 常见坑:把 allowDiskUse 当性能开关。溢写磁盘比内存慢一个数量级,它只是避免失败,定位到慢就该回头改阶段顺序。

事故复盘:40 秒接口的三轮优化

第一轮只把 match 提到 unwind 之前,接口从 40 秒降到 6 秒——过滤发生在放大之前,扫描量按平均每单十个商品数直接砍掉一个乘数。第二轮给 createdAt 建索引并把 match 的条件对齐索引字段,6 秒降到 1.8 秒,此时瓶颈转移到 group:当天订单仍有一百多万行进分组。第三轮上了 $merge 预聚合,按分钟粒度把城市 GMV 增量写入汇总集合,大屏直接读汇总,接口稳定在 30 毫秒内。三轮优化对应三个不同的瓶颈层次:扫描量、排序与分组、架构——单轮思维("加个索引")在这里解决不了问题,逐层测量才走得通。

顺带补一个排查细节:当时团队一度怀疑是网络与前端渲染问题,因为聚合在数据库侧异步执行、日志里只有慢查询阈值以上的记录,30 秒的执行被拆成多条日志。把 profiler 的慢阈值临时降到 100 毫秒后,耗时分布一目了然,定位方向才回到管道本身。慢查询分析的系统方法在第 10 章展开。

$facet 与表达式优化的边界

facet 允许一条管道同时输出多组结果(如列表加总数、分面计数),大屏类接口很爱用,但它有个硬约束:所有子管道共享进入 facet 前的数据流,且 facet 整体算一个阶段、共享同一份 100MB 限制。数据量大时更稳的做法是拆成多次查询或预聚合,不要指望 facet 一条管道包打天下。

表达式层面也有性价比差异。cond 与 switch 做条件标记轻量;regexMatch 每文档执行正则,放在过滤靠后的位置就是性能黑洞;数组操作符 filter 比"unwind 再 match 再 group 回去"的写法快得多,因为它不放大文档数。一个实用判断标准:凡是会改变文档数量的阶段(unwind、group、limit),比只改变文档内容的阶段(project、$addFields)贵一个量级,优化优先动前者。

// 数组内过滤:$filter 不放大,unwind 写法放大 10 倍再收敛 db.orders.aggregate([ { $project: { items: { $filter: { input: "$items", as: "i", cond: { $gt: ["$$i.amount", 100] } } } } } ]);

与相邻概念的辨析

聚合管道常被拿来和三个东西比较,分清楚能省很多纠结。和 find 比:find 是单阶段过滤加投影,聚合是其超集,简单查询没必要用聚合,可读性与优化器待遇都更差。和 MapReduce 比:旧版 MapReduce 已被官方废弃,任何还在用它的代码都该迁移到管道,性能与维护性都是数量级的差距。和 SQL 比:group 对应 GROUP BY、lookup 对应 JOIN、$match 对应 WHERE,语义映射清晰,但执行模型不同——管道没有全局优化器帮你重排阶段,顺序即执行计划,写的人要自己当优化器。这也是为什么本章两起事故的根因都是"顺序",而 SQL 里同样的查询数据库多半会自动优化掉。

本节要点回顾

  • 先过滤再展开,$unwind 之前的每一分收敛都是乘法收益;
  • 管道开头的 match 加 sort 是唯一能吃索引的位置;
  • $merge 预聚合是高频报表的标准解;
  • allowDiskUse 是保险丝不是加速器。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U