正确多字段分组需用{_id:{field1:"$field1",field2:"$field2"}}对象形式,字符串或数组写法会导致全归一组;累加器命名应避免与原始字段同名,空值需预处理,高基数分组键须优化。

用 _id 对象实现多字段分组
多字段分组不是把多个字段名拼在一起,而是把它们组织成一个对象字面量作为 _id 的值。MongoDB 会按该对象的**结构+值**做唯一性判断,等价于 SQL 中的 GROUP BY field1, field2。
常见错误是写成 _id: "$field1, $field2"(字符串)或 _id: ["$field1", "$field2"](数组),这两种写法都会导致所有文档被归为同一组,因为字符串和数组本身是常量表达式,不参与字段取值。
-
_id: { category: "$category", status: "$status" }✅ 正确:两个字段同时参与分组 -
_id: "$category"❌ 单字段,即使后面加了其他累加器也不算“多字段分组” -
_id: null或_id: 1❌ 所有文档强行合并为一条结果
累加器字段命名不能和原始字段同名
在 $group 阶段里,输出字段名(如 total、count)如果和输入文档中已存在的字段名重复,不会报错,但会导致后续阶段读不到原始值——因为 $group 输出的文档只保留 _id 和你显式声明的累加字段,原始字段默认丢弃。
比如原始文档有 { amount: 100, status: "shipped" },若写成 amount: { $sum: "$amount" },那输出里就只有 _id 和 amount 字段,原始 amount 已不可见;如果后续还想用原始 status 做过滤,就必须提前在 $group 前用 $addFields 或 $project 保存副本。
- 推荐命名习惯:
sum_amount、avg_price、doc_count,避免歧义 - 如果真要保留原始字段值(如取每组第一个
status),用first_status: { $first: "$status" } -
$push和$addToSet适合保留组内原始值集合,但要注意内存开销
空值或缺失字段会影响分组结果一致性
$group 对 null、undefined、缺失字段的处理是一致的:它们会被视为相同值。也就是说,{ category: null }、{ category: undefined }、{}(无 category 字段)这三类文档,在 _id: "$category" 分组下会落到同一个组里。
这在统计报表中容易引发数据偏差。例如按用户地区分组时,大量缺失 region 的记录突然堆进 “null 组”,掩盖真实分布。
- 预防方式:在
$group前加$match过滤掉空值,如{ region: { $ne: null } } - 或用
$ifNull统一补默认值:_id: { region: { $ifNull: ["$region", "unknown"] } } - 注意
$ifNull不会修改原始字段,只影响分组键生成
性能敏感场景必须控制分组基数
$group 是阻塞阶段,必须收齐全部输入才能输出。如果 _id 表达式产生的唯一键太多(比如用 _id: "$_id" 或高基数时间戳字段),不仅内存占用飙升,还可能触发 Sort key too large 错误或 OOM kill。
真实业务中,分组前应尽量缩小数据集:用 $match 提前过滤、用 $project 剔除无关字段、避免在分组键里引入未索引字段(尤其是文本或嵌套对象)。
- 高风险组合:
_id: "$email"(百万级唯一值)、_id: "$createdAt"(毫秒级精度) - 可优化为:
_id: { day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } } - 聚合命令加上
allowDiskUse: true参数,防止内存溢出中断,但会降低速度
_id 表达式的实际产出分布——它既决定结果正确性,也直接卡住整个管道的吞吐。上线前务必用 $limit + explain("executionStats") 看真实分组数量和内存消耗。

















