日志分析用聚合管道,核心是$match放最前、合理设$group的_id、提前建复合索引;否则查得慢、结果错、易爆BSON超限错误。

直接上结论:日志分析用聚合管道,核心不是“能不能做”,而是“在哪加 $match、怎么设 $group 的 _id、要不要提前建索引”。没这三步,查得慢、结果错、还容易爆 BSON document too large 错误。
为什么日志场景下 $match 必须放在最前面?
日志集合动辄几千万甚至上亿文档,不先过滤就进管道,后续所有阶段($group、$sort)都在处理无效数据。比如查“过去一小时的 5xx 错误”,如果把 $match 放在 $unwind 后面,可能先展开所有日志再筛,内存和耗时翻倍。
-
$match阶段能利用索引(尤其是时间字段 + 状态字段的复合索引),大幅减少扫描文档数 - 错误写法:
{$unwind: "$tags"}, {$match: {"status": 500}}—— 先展开再筛,已膨胀的数据无法被索引加速 - 正确顺序:
{$match: {timestamp: {$gte: ISODate("..."), $lt: ISODate("...")}, status: 500}},再$unwind或$group - 注意:
$match中不能用聚合表达式(如$expr)替代简单字段匹配,否则索引失效
$group 的 _id 怎么设计才不丢维度?
日志分析常需多维下钻(比如按服务名+错误码+分钟粒度统计),但 _id 是唯一键,填错就合并了不该合并的记录。
- 单维度统计:
_id: "$service_name" - 多维度组合:
_id: {service: "$service_name", code: "$status", minute: {$dateToString: {format: "%Y-%m-%d %H:%M", date: "$timestamp"}}} - 别写
_id: null做全局汇总——除非真要总数,否则会丢失所有分组上下文 - 如果字段可能为空(如
user_id缺失),_id里直接引用会导致该文档被归到null组,建议用$ifNull处理:user: {$ifNull: ["$user_id", "anonymous"]}
什么时候必须用 $facet,而不是拼多个 pipeline?
当一次请求要同时返回“错误率趋势”“Top 5 异常服务”“各状态码分布”三类指标时,发三次聚合请求不仅网络开销大,且时间窗口可能不一致。这时 $facet 是唯一靠谱解法。
-
$facet允许在一个聚合中并行执行多个子管道,共享同一份$match过滤结果 - 典型结构:
{$facet: {errors_by_minute: [...], top_services: [...], status_dist: [...]}} - 坑点:每个子管道仍受 16MB BSON 限制,若某子结果过大(如未
$limit的 Top N),整个$facet会失败 - 安全做法:所有子管道末尾加
{$limit: 100},并在应用层判断是否截断
真正卡住人的往往不是语法,而是时间字段类型不一致(字符串 vs ISODate)、TTL 索引没配导致历史日志堆积拖慢查询、或者 $group 后忘了 $project 剔除无用字段让传输体积失控。这些细节不调,再漂亮的 pipeline 也跑不稳。


















