图 1:文档流过管道,每过一段数据量就缩一截。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「什么是聚合」「聚合管道怎么工作」是 MongoDB 面试的常客。AI 应用的行为日志分析、标注数据统计常常落在 MongoDB 上,聚合管道就是主要的分析入口,值得讲透。
先厘清「聚合」到底指什么。
题目原话常常只有「什么是聚合」四个字,回答时先给定义再谈机制。聚合是 MongoDB 里对一批文档做分组统计、变换、整形的统称,对应关系型数据库里 GROUP BY、COUNT、SUM、AVG 那一族能力。它有两代实现:早年是 map-reduce,灵活但写起来重、性能差,从 5.0 版本起已被官方弃用;现在的标准答案就是聚合管道——声明式地列出阶段,由引擎负责执行和优化。面试里把「map-reduce 已退场、管道是主流」点出来,能体现信息是新近核对过的。
把管道想成传送带:文档流逐段加工。
聚合管道由一个或多个阶段(stage)组成,文档从集合流入第一个阶段,每个阶段对输入文档做一次变换,输出再交给下一阶段。关键点:阶段不是一进一出,$match 会扔掉文档,$group 会把一百条折叠成一组,$unwind 会把一条拆成多条。所以管道里流动的「数据量」是逐段变化的,这也是性能话题的入口。
拿一个典型分析需求串起来:从行为日志里找近 7 天成功请求的 Top 5 品类。管道写成四段:
- $match:过滤出近 7 天且 status 为 ok 的文档,对应 SQL 的 WHERE;
- $group:按品类分组、对金额求和,对应 GROUP BY 加 SUM 聚合;
- $sort:按总金额降序,对应 ORDER BY;
- $limit:只取前 5,对应 LIMIT。
模拟一遍,看数据量怎么收缩。
下面用 Node.js 22.15.0 模拟这条管道(node pipeline.js),造 1200 条合成日志,逐段打印剩余条数:
const DAY = 86400000, now = Date.UTC(2026, 10, 20);
const cats = ['搜索', '推荐', '支付', '登录', '上传', '下载', '评论', '分享'];
const docs = [];
for (let i = 0; i < 96; i++) // 近 7 天的 ok 日志
docs.push({ ts: now - (i % 7) * DAY, category: cats[i % 8],
status: 'ok', amount: (i % 8 + 1) * 10 + Math.floor(i / 8) });
for (let i = 0; i < 1104; i++) // 更早的日志,部分 fail
docs.push({ ts: now - (8 + (i % 42)) * DAY, category: cats[i % 8],
status: i % 5 === 0 ? 'fail' : 'ok', amount: (i % 13) + 1 });
const s1 = docs.filter(d => d.ts >= now - 7 * DAY && d.status === 'ok'); // $match
const acc = new Map();
for (const d of s1) acc.set(d.category, (acc.get(d.category) || 0) + d.amount);
const s2 = [...acc.entries()].map(([category, total]) => ({ category, total })); // $group
const s3 = s2.sort((a, b) => b.total - a.total).slice(0, 5); // $sort+$limit
console.log(docs.length, s1.length, s2.length, s3.length);
console.log(s3.map(g => `${g.category}=${g.total}`).join(' '));
输出:
1200 96 8 5
分享=1026 评论=906 下载=786 上传=666 登录=546
1200 条进管道,$match 砍到 96,$group 折叠成 8 组,$sort 加 $limit 留下 5 条。真实 MongoDB 里这条管道的写法是 [{ $match: {...} }, { $group: { _id: '$category', total: { $sum: '$amount' } } }, { $sort: { total: -1 } }, { $limit: 5 }],语义与模拟一一对应。
管道顺序为什么就是性能。
每段处理的数据量取决于上一段吐多少,所以总代价大致由「最胖的那段」决定。原则一句话:让数据尽早变小。$match、$limit 尽量靠前——把 $match 放在 $group 后面,等于让分组去折叠一堆注定被扔掉的文档,白做工。
好消息是 MongoDB 自己会帮一把:优化器会把不依赖计算字段的 $match 自动前移、把紧跟 $sort 的 $limit 合并进排序(排序时只维护前 N 条,内存里不留全量)、把相邻的同种阶段合并。管道开头的 $match 和 $sort 还能走索引。但自动优化有边界——它不知道你的数据分布,也不知道哪些过滤条件其实可以合并,所以写法上仍要主动按「先过滤、后加工、最后整形」的顺序组织,再用 explain 看优化器实际改写成了什么样。
还有两个容易踩的点:一是 $group 的 _id 决定分组粒度,写错字段(比如拿时间戳原文分组)会把组数打爆;二是管道中间结果有内存限制,大结果集排序或分组要评估 allowDiskUse 或干脆前置 $match 瘦身。
图 2:数据量竖条逐级收缩,下方是三张与 SQL 语义的对照卡。
回到面试,推荐的答题骨架:先一句话定义「文档流过多阶段、每段一变换」,再用一条具体管道把 $match/$group/$sort/$limit 与 SQL 语义对上,最后讲顺序即性能和优化器的自动重排。如果能补一句「 explain 验证优化结果」,就有工程味了。你在生产里写过最复杂的管道有几段?有没有被 $group 的分组键坑过?欢迎分享。