$match必须前置,否则报表易卡在查得慢、内存爆、结果错三连;因后置会导致全量展开分组再过滤,如先$unwind炸开全部items再$match,中间文档暴增上万条。

为什么 $match 必须放在聚合管道最前面
不前置 $match,报表就容易卡在“查得慢、内存爆、结果错”三连——尤其当集合有几十万文档、每天只查单日数据时,后置过滤会让整个管道先展开、分组、排序完再砍掉 99% 的数据。
常见错误现象:db.orders.aggregate([ { $unwind: "$items" }, { $group: { _id: "$date", total: { $sum: "$amount" } } }, { $match: { _id: { $gte: ISODate("2026-09-06"), $lt: ISODate("2026-09-07") } } } ])。这里 $match 写在最后,$unwind 已把全量订单的 items 全部炸开,哪怕你只要昨天 200 条订单,也可能生成上万中间文档。
- 正确做法:第一阶段就用
$match锁定日期范围,例如{ date: { $gte: ISODate("2026-09-06"), $lt: ISODate("2026-09-07") } } - 如果
date字段没建索引,加explain()会看到"totalDocsExamined": 0变成几十万——索引必须覆盖date或复合字段如{ date: 1, status: 1 } - 注意:若日期字段藏在嵌套数组里(如
logs.time),$match确实要放$unwind后,但这种情况极少;日常业务表应把date提到顶层
怎么用 $group 按天分组并算出核心指标
$group 的 _id 决定“一天”怎么定义,不是简单写 "$date" 就完事——MongoDB 的 Date 类型带时分秒,直接分组会导致同一天被拆成几十个桶。
使用场景:你要的是“2026-09-06 这一整天”的订单数、支付金额、新客数、退款率等。
- 安全提取日期:用
$dateToString标准化,例如_id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } } - 条件计数别漏
$cond:新客数不能写{ $sum: 1 },得用{ $sum: { $cond: [ { $eq: ["$is_new", true] }, 1, 0 ] } } - 退款率需双聚合:先
$sum退款金额和总金额,再用$divide计算,避免除零,例如refund_rate: { $divide: [ "$refund_total", { $add: ["$paid_total", 0.0001] } ] } - 空值保护:如果某天没退款,
$refund_total是null,$divide会返回null,前端可能崩;建议统一用$ifNull替换,如{ $ifNull: ["$refund_total", 0] }
如何处理含嵌套明细(如订单项)的每日汇总
订单表带 items 数组,要算“每日 SKU 销量 Top10”,就必须 $unwind,但直接炸开极易引发数据膨胀或空数组报错。
错误示例:{ $unwind: "$items" } 遇到 items: [] 或缺失字段直接中断管道。
- 必须加参数:
{ $unwind: { path: "$items", preserveNullAndEmptyArrays: true } } - 炸开后立刻过滤 null:
{ $match: { "items.sku": { $exists: true } } },否则$group会把null当有效值 - 别在
$unwind后直接$group:先$project提取关键字段,比如{ sku: "$items.sku", qty: "$items.qty", price: "$items.price" },去掉无关层级,减少内存占用 - 性能提醒:如果单订单平均含 50+ items,且每日订单超 1 万,考虑改用
$reduce在数组内统计,例如{ $reduce: { input: "$items", initialValue: 0, in: { $add: ["$$value", "$$this.qty"] } } }直接算总销量,跳过$unwind
为什么 $sort + $limit 要放在 $group 后面
报表常需“每日销量 Top10 SKU”,很多人把 $sort 放在 $unwind 后、$group 前,结果是按每个 item 排序,不是按汇总后销量排——逻辑全错。
典型错误:$unwind → $sort → $group,这只会让单条明细按价格排,对 Top10 毫无意义。
- 正确顺序:先
$group出每个sku的日销量,再$sort: { daily_qty: -1 },最后$limit: 10 - 如果要“每日 Top10”且输出带日期字段,
$group的_id得是复合键,例如_id: { date_str: "$date_str", sku: "$sku" },否则$sort无法区分不同天的同一 SKU - 注意内存限制:
$sort在内存中完成,若未加$limit且分组后数据量大(如 10 万 SKU),可能触发"errmsg": "Sort exceeded memory limit...";务必在$sort后紧跟$limit
真正难的不是写对语法,而是想清楚“哪一步该减数据量、哪一步该保结构、哪一步容不得 null”——尤其是嵌套数组和日期标准化,错一个参数,报表数字就偏几个百分点,还很难排查。











