聚合管道是什么
聚合管道是 MongoDB 的通用计算框架:db.collection.aggregate([...]) 把文档流依次送进各阶段,每个阶段的输出就是下一阶段的输入。阶段按用途分四类——过滤($match)、变换($project/$addFields)、重组($group/$unwind/$sort)、外部数据($lookup/$unionWith/$out)。它是做统计、报表与 ETL 的主力工具,$match 的条件写法与 find 完全通用。
基本信息
| 语法 | db.collection.aggregate([ stage, ... ], options) |
|---|---|
| 常用阶段 | $match / $group / $project / $sort / $limit / $skip / $unwind / $lookup |
| 常用选项 | allowDiskUse / maxTimeMS / collation / explain / hint |
| 内存上限 | 单阶段输出上限 100MB,超限需 allowDiskUse: true |
| 返回 | 默认返回游标,也可用 $out / $merge 落库 |
典型使用场景
- 多维统计报表:按日、按渠道、按状态分组汇总
- 明细展开再聚合:$unwind 拆数组后按元素统计
- 跨集合补数据:$lookup 关联用户或商品维表
- 数据搬迁与清洗:$project 改结构后 $merge 写回新集合
- 口径核对:与 find 结果交叉验证,确认过滤条件正确
- 性能诊断:用 explain 观察 $match 是否被下推到索引扫描
端到端示例
端到端示例
db.orders.aggregate([
{ $match: { status: "paid", ts: { $gte: ISODate("2026-01-01T00:00:00Z") } } }, # 先过滤,减少后续数据量
{ $group: { _id: "$sku", totalQty: { $sum: "$qty" }, orders: { $sum: 1 } } },
{ $sort: { totalQty: -1 } }, # 按销量降序
{ $limit: 5 } # 只看前 5 组
])阶段速查
| 阶段 | 作用 | 注意点 |
|---|---|---|
| $match | 按条件筛选,尽量放最前 | 写法同 find,可走索引 |
| $group | 分组并计算累加器 | 大分组需 allowDiskUse |
| $project | 挑字段、算新字段、改名 | 包含与排除不能混写 |
| $unwind | 把数组拆成多条文档 | 空数组会丢文档,可保留 |
| $lookup | 左外连接另一集合 | from 侧连接字段要有索引 |
| $merge / $out | 把结果写回集合 | $out 覆盖整表,$merge 可增量 |
$match 与 $sort 放在管道最前面才能借上索引;一旦被 $project 或 $unwind 改变了文档结构,后续阶段就只能走内存处理,成本立刻上升。
用 aggregate 的 { explain: true } 选项或 .explain("executionStats") 观察执行计划,重点确认 $match 是否被优化器下推到 IXSCAN 而不是 COLLSCAN。
管道默认每个阶段最多用 100MB 内存,超出会直接报错;allowDiskUse: true 会落临时文件,能跑通但磁盘 IO 会明显上升,属于临时手段。
长管道难排错时用中间落库法:把前半段结果写进临时集合,再对临时集合写后半段,这样每一步的输入都能单独核对。
性能与排错要点
- 先用 $match 缩小范围,再进入 $group 与 $sort,是全链路最省资源的顺序
- $group 的 _id 分组键尽量用低基数、语义清晰的字段,避免用整个文档当键
- $lookup 之后紧跟 $match 时,把条件写进 $lookup 的 pipeline 形式可减少返回的匹配文档
- $facet 可以在一趟扫描里产出多个维度的结果,适合报表类一次出多组统计
- $sort 之前先 $limit 只是取前 N,真正需要全局排序时排序键要有索引
- 用 maxTimeMS 给线上临时查询设超时,避免长管道把资源占满
关联后落库
关联后落库
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $lookup: { from: "products", localField: "sku", foreignField: "sku", as: "p" } },
{ $unwind: "$p" }, # 拍平单元素数组
{ $project: { _id: 0, sku: 1, name: "$p.name", totalQty: 1 } },
{ $merge: { into: "report_sku", whenMatched: "replace", whenNotMatched: "insert" } }
])$facet 允许在一趟数据扫描中并行跑多条子管道,做多维度报表时比拆成多次查询更省 IO,但每个子管道的中间结果仍受 100MB 内存限制约束。
常用累加器与表达式
- $sum 求和或计数,$avg / $min / $max 取组内统计量
- $push 收集成数组,$addToSet 去重收集
- $first / $last 取每组首尾值,只有排序后语义才确定
- $count 是较新版本提供的计数累加器,等价于 $sum 常量 1
- $cond / $switch / $ifNull 做条件分支与空值兜底
- $dateToString / $dateTrunc 处理按日按月的分桶聚合
$sort 在管道内排序时的内存上限同样是 100MB,且排序键若产生于 $group 之后就无法借索引,只能落内存或磁盘,报表设计要尽量让排序键来自可预先聚合的字段。
$out 与 $merge 必须是管道的最后一个阶段,写在中间会被直接拒绝;调试阶段的中间结果先用 $limit 抽查,确认口径再放开全量。