4.7 聚合框架:文档流水线


4.7 聚合框架:文档流水线

CRUD 解决"取数据",聚合框架解决"算数据"——过滤、分组、变形、连接,全在服务端以流水线方式完成。它让 MongoDB 能独立承担中等复杂度的统计分析(呼应 1.3 的查询能力讨论),不必事事外迁分析栈。本节讲流水线心智模型、常用阶段与性能纪律,并以一个完整报表需求贯穿。

流水线心智模型

聚合(Aggregation Pipeline)的本质是一条文档流水线:文档从一端流入,依次经过若干处理阶段(Stage),每个阶段吞入文档流、吐出变换后的文档流,最终输出结果。与 SQL 对着看心智模型立刻建立:match 是 WHERE、group 是 GROUP BY、sort 是 ORDER BY、project 是 SELECT 列裁剪、$limit 是 LIMIT。

// 每日各商品销量统计:过滤 → 分组 → 排序 → 限量 db.orders.aggregate([ { $match: { createdAt: { $gte: ISODate("2026-08-01"), $lt: ISODate("2026-09-01") } } }, { $unwind: "$items" }, // 订单项数组展开成逐条文档 { $group: { _id: { sku: "$items.sku" }, totalQty: { $sum: "$items.qty" }, totalAmt: { $sum: "$items.price" }, orderCnt: { $sum: 1 } } }, { $sort: { totalAmt: -1 } }, { $limit: 10 } ])

高频阶段清单

五个阶段覆盖八成需求。match** 尽早过滤——流水线纪律第一条:越早把文档量砍小,后续每个阶段都受益;且打头阵的 match 能利用索引(4.4 的规则原样适用)。group** 按 _id 表达式分组并累计(sum/avg/max/push),分组键可以是嵌套字段甚至多字段组合。**unwind** 把数组字段展开成多份文档,是"订单内嵌订单项"这类嵌套模型做统计的必经之路。project** 裁剪与变形字段(重命名、计算字段),收窄后续阶段的文档体积。**lookup 跨集合关联(左外连接语义)——文档门派少数的关联手段,注意它是流水线里最贵的阶段之一,数据量大时先想建模能不能避免它(3.5 的冗余手法)。

演练:商家周报的完整实现

背景:订单集合(订单项内嵌,4.2 建模遗留的合理结构),需求是给每个商家生成周报:本周订单数、总销售额、销量前三的商品、客单价。数据量:周订单 2000 万。

操作:第一步把"商家维度"从订单项冗余到订单文档(商家 ID 下沉),使 group 分组键直接可得、match 可命中商家索引;第二步设计流水线:match(时间 + 商家,走复合索引)→ unwind 订单项 → 两个 group(先按商品汇总排序取前三,再按商家汇总)→ project 整形输出客单价(总销售额除以订单数);第三步在测试环境 explain 验证打头的 $match 走 IXSCAN、中间阶段文档量逐级递减。

结果:周报生成耗时从最初版本(match 放在 unwind 之后)的 40 秒降到 3 秒——改动只有把 $match 提到最前并补了索引。

解读:性能差异完全来自阶段顺序——先展开后过滤,2000 万订单被展开成上亿订单项文档后才过滤;先过滤后展开,进入展开环节的文档已缩到千分之一。流水线不是各阶段独立计价的,顺序就是性能。另注意客单价这类派生指标在 $project 里现算,不落库、不冗余——快照语义才冗余,派生指标现算(3.5 的原则反着用)。

变式:若报表维度增长到几十个、或需要跨月大范围扫描,服务端聚合也吃力,正确演进是夜间预聚合到统计集合(物化视图),在线查询只碰小表——聚合框架负责算,预聚合负责扛量。

易错点

第一个是**lookup 滥用**:把关系型的多表连接照搬到聚合里,三个 lookup 串起来在大集合上近乎不可用;文档建模(嵌入或冗余)能消掉绝大多数 lookup。第二个是**在 group 前忘掉 project**:带着全量字段分组,内存与网络白白多扛几倍体积。第三个是**忽略聚合的内存上限**:group/sort 默认允许占用内存有上限,超限直接报错——大数据量的 sort 要建索引配合(阶段能从索引读序)或分批处理。

流水线阶段速查表

聚合框架把数据处理表达成一条流水线:文档依次流过各个阶段,每个阶段做一次变换。常用的阶段如下。

阶段 作用 典型用法
$match 过滤文档 尽量放最前,能利用索引并减少后续数据量
$project 挑选与计算字段 只保留需要的字段,减小管道中的数据量
$group 分组聚合 求和、计数、收集数组
sort | 排序 | 放 group 之后;大数据量排序受内存限制
$lookup 跨集合关联 类似左连接,代价高,慎用
$unwind 拆开数组 把数组元素变成独立文档再做统计
$facet 多路并行 一次查询产出多个统计结果(如总数 + 分组)
out / merge 写出结果 把聚合结果写入集合,用于预计算

演练:一条完整的月度销售统计

需求:统计 2026 年 8 月各城市的销售额、订单数与客单价,只统计已支付订单,输出按销售额倒序的前 10 个城市。

db.orders.aggregate([ // 1) 先用索引过滤,把进入管道的数据量压到最小 { $match: { status: "PAID", created_at: { $gte: ISODate("2026-08-01"), $lt: ISODate("2026-09-01") } } }, // 2) 只保留后续需要的字段 { $project: { city: 1, total_amount: 1 } }, // 3) 按城市分组聚合 { $group: { _id: "$city", gmv: { $sum: "$total_amount" }, orders: { $sum: 1 } } }, // 4) 计算派生指标 { $addFields: { avg_order_value: { $round: [ { $divide: ["$gmv", "$orders"] }, 2 ] } } }, { $sort: { gmv: -1 } }, { $limit: 10 } ])

性能与限制

**match 要尽量前置。** 放在管道开头的 match 能用上索引,相当于把"全表扫描后的过滤"变成"索引扫描"。顺序调换可能让耗时差一个数量级。

排序与分组有内存上限(早期版本 100MB)。超限时要么加 allowDiskUse 允许落盘(变慢但能跑完),要么在 sort 前加 match 减少数据量。更好的做法是为常用聚合建专门的索引,让排序走索引。

lookup 是双刃剑。** 它能做跨集合关联,但代价接近 JOIN——在分片集群上尤其昂贵,因为关联可能要跨分片。设计上的正确做法是:**能用嵌入解决的关联就不要用 lookup;必须用的时候,先确认被关联集合的关联字段有索引,并尽量缩小左集合。

预计算代替实时聚合。 对于固定口径的统计(日报、月报),用 $merge 把结果定期写入结果集合,查询时直接读结果。这比每次实时聚合快几个数量级,代价是数据有延迟——多数报表场景完全能接受。

本节要点回顾

  • 流水线心智:文档流过 match/group/unwind/project 等阶段,与 SQL 一一对照易上手。
  • 纪律第一条:$match 尽早且走索引,后续阶段全跟着受益。
  • $unwind 是嵌套数组统计的必经之路;展开前先过滤。
  • $lookup 贵:优先用建模(嵌入/冗余)消关联,实在要连要控量。
  • 维度膨胀时升级为预聚合 + 物化视图,聚合框架管算、统计表管扛。

分析能力补齐,最后一节回到那个"标志性缺席"的能力:事务。


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