MongoDB聚合管道实战:$match过滤与$group统计分析优化

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/

(0)
小编小编
上一篇 8小时前
下一篇 8小时前

相关推荐