数据库 MongoDB

MongoDB 聚合管道实录:一次日志统计需求

需求很朴素:客户端崩溃日志按天、按渠道、按版本统计崩溃率。数据在 MongoDB 里,一天大概两百万条。第一版我用了三次独立查询,接口耗时 8 秒;第二版改成一条聚合管道,1.2 秒。记录一下改造过程。

关键在于 $facet:它允许一条管道里同时跑多条子管道,共享前面 $match 的过滤结果。而 $match 恰恰是整个查询里最贵的部分(时间范围过滤命中两百万条),提前做一次、三次复用,成本直接砍到三分之一。

db.crash.aggregate([
  { $match: { ts: { $gte: start, $lt: end } } },
  { $facet: {
      byDay: [
        { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$ts" } },
                    total: { $sum: 1 },
                    fatal: { $sum: { $cond: [{ $eq: ["$level", "fatal"] }, 1, 0] } } } },
        { $sort: { _id: 1 } }
      ],
      byChannel: [
        { $group: { _id: "$channel", total: { $sum: 1 } } }
      ],
      byVersion: [
        { $group: { _id: "$appver", total: { $sum: 1 } } },
        { $sort: { total: -1 } },
        { $limit: 20 }
      ]
  }}
])

两个容易忽略的点。其一,$facet 子管道不能再有自己的 $match 索引,所有索引只能在最外层的 $match 上生效——所以复合索引要按最外层过滤条件建:{ ts: 1, channel: 1 }。其二,$facet 会把结果攒在内存里,子管道输出太大会撞 100MB 的限制,每个分支加 $limit 是好习惯。

另外 $dateToString 每条文档都要执行一次,两百万条下来 CPU 有点吃不消。后来把天粒度冗余成写入时的 day 字段(字符串),$group 直接按它分组,又省了 300ms。空间换时间,永远不过时。