MongoDB聚合管道(Aggregation Pipeline)是文档数据库做统计分析的核心工具,订单报表、用户行为分析、日志统计都靠它。同样的数据量,管道顺序不同、索引利用不同,执行时间可以从毫秒级差到分钟级。$match前置、$project裁剪、$group利用索引分组,三个环节按规则设计,聚合性能可以稳定在一个数量级之内。
聚合管道基础结构与执行顺序
管道由多个stage顺序执行,前一个stage的输出是后一个的输入。统计每个城市各状态订单数和金额的典型管道:
db.orders.aggregate([
{ $match: {
create_time: { $gte: ISODate("2026-09-01"), $lt: ISODate("2026-09-16") },
status: { $in: ["paid", "shipped", "done"] }
}},
{ $group: {
_id: "$city",
order_count: { $sum: 1 },
total_amount: { $sum: "$amount" },
avg_amount: { $avg: "$amount" }
}},
{ $sort: { total_amount: -1 } },
{ $limit: 20 },
{ $project: {
_id: 0, city: "$_id",
order_count: 1, total_amount: 1, avg_amount: 1
}}
])
$match放最前面的意义在索引:create_time和status的组合索引可以被$match利用,文档在进入管道前先在索引层过滤,千万级集合只扫命中部分。$match挪到$group之后,MongoDB必须全集合扫描再分组,索引完全失效。规划管道的通用法则:过滤尽量前置,投影尽早裁剪字段,$sort在$group前能走索引就单独执行。
$match与索引联动优化
$match要吃到索引,查询条件写法有讲究。$expr里包字段比较走不了索引:
// 无法利用索引
{ $match: { $expr: { $eq: ["$status", "paid"] } } }
// 走索引的等价写法
{ $match: { status: "paid" } }
// explain验证索引使用情况
db.orders.aggregate([
{ $match: { status: "paid", create_time: { $gte: ISODate("2026-09-01") } } }
], { explain: true })
// winningPlan里出现 IXSCAN 说明索引生效,COLLSCAN 是全表扫描
验证手段是explain。explain输出的winningPlan.stage为IXSCAN表示索引扫描,COLLSCAN表示集合扫描。多字段条件按ESR原则建索引:等值(Equality)字段在前,排序(Sort)字段居中,范围(Range)字段在后。status等值+create_time范围的查询,索引应该是{status: 1, create_time: 1},把create_time放前面会导致status过滤走不了索引前缀。
$group阶段性能与allowDiskUse
$group把分组键相同的文档汇聚到一个节点处理,内存硬限制100MB,超限直接报错。分组的基数(不同_id取值数量)决定内存压力:按user_id分组千万级文档,基数几百万,必超限制。两种解法:
// 解法一:允许溢出到磁盘,性能下降但能跑完
db.orders.aggregate(
[ { $group: { _id: "$user_id", cnt: { $sum: 1 } } } ],
{ allowDiskUse: true }
)
// 解法二:数据量极大时先采样或预聚合
// 写入时用$merge把按天统计结果固化成新集合
{ $merge: { into: "orders_daily_stats", whenMatched: "replace" } }
allowDiskUse把溢出分组写入磁盘临时文件,吞吐下降明显,适合夜间批任务。实时查询场景改为预聚合:定时任务跑管道用$merge落统计表,在线查询直接查汇总结果,查询延迟从秒级降到毫秒级。$merge在MongoDB 4.4后可用,写入目标集合还能维护索引,是数仓分层思路在MongoDB上的落地。
复杂统计场景:$lookup关联与$facet多结果
订单关联用户信息做报表,$lookup相当于SQL的JOIN,但实现机制是嵌套循环,右集合无索引时性能急剧劣化:
db.orders.aggregate([
{ $match: { create_time: { $gte: ISODate("2026-09-15") } } },
{ $lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" },
{ $group: {
_id: "$user.level",
order_cnt: { $sum: 1 }
}}
])
users表的_id自带索引,$lookup的foreignField对齐_id可以走索引,左集合每条文档关联查找是索引级别。关联普通字段必须先给右集合建索引。$facet一条管道同时输出多个统计结果,适合报表页一次请求拿多组数据:
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $facet: {
"by_city": [ { $group: { _id: "$city", cnt: { $sum: 1 } } } ],
"by_amount": [ { $bucket: { groupBy: "$amount", boundaries: [0,100,500,1000], default: "1000+" } } ],
"total": [ { $count: "n" } ]
}}
])
$facet对输入执行多个子管道,$match只跑一次,多个报表共享过滤结果。注意$facet内不能再用会影响输入流的stage做外层过滤,分桶统计用$bucket自动分段,比手写多层$group简洁。聚合管道的优化心法归结为:让索引干过滤的活,让$group干汇总的活,磁盘和预聚合兜住大数据量。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mongodb-ju-he-guan-dao-shi-zhan-match-guo-lyu-yu-group-tong/