$group 多字段分组需将字段组合为对象作为 _id,如 {a: "$a", b: "$b"};不可拼接字符串或漏写 $;分组后须用累加器(如 $first、$sum)提取值;索引字段顺序须与 _id 键序一致才能高效支持后续排序。

用 $group 对多个字段做分组,核心是把多个字段组合成一个唯一标识
直接把多个字段塞进 _id 里就行,不用嵌套或额外聚合阶段。MongoDB 的 _id 支持对象,所以可以写成 { field1: "$field1", field2: "$field2" } 这种结构——它会自动按这两个字段的组合值分组。
常见错误是试图用 $and 或拼接字符串(比如 "$field1_$field2"),这会导致类型不一致、排序错乱、空值处理异常,而且无法利用索引。
- 如果某个字段可能为
null,组合对象里对应键仍保留,不会被忽略 - 字段顺序影响分组结果:
{ a: "$a", b: "$b" }和{ b: "$b", a: "$a" }是不同_id - 分组后想查原始字段值?必须用
$first、$last或$push显式提取,不能直接引用$field1
$group 中的 _id 字段必须是表达式,不能是字面量
写 { _id: "abc" } 会把所有文档归到同一个组,不是按字段值分组。哪怕只按一个字段分组,也要写成 { _id: "$status" },而不是 { _id: "status" }。
容易混淆的是 shell 里敲命令时漏掉美元符,比如误写成 _id: "user_id",结果就是全归一组,还查不出错——因为语法合法,只是逻辑不对。
- 字段名带点号(如
address.city)必须用"$address.city",不能写address.city - 想按计算值分组?比如按年份分组日期字段:
_id: { year: { $year: "$created_at" } } - 如果字段不存在,对应值在
_id对象中为null,该文档仍参与分组
多字段分组后聚合值的提取方式直接影响性能和语义
分组后的聚合字段(比如计数、求和)要靠累加器函数,但每个字段的语义得自己定清楚。比如 $sum: 1 是计数,$sum: "$amount" 才是求和;用 $first 取某字段值时,结果取决于文档输入顺序,不可靠。
实际写的时候,别默认用 $first 提取非聚合字段——除非你明确控制了排序,否则结果随机。更稳妥的做法是提前 $sort 再 $group,或者改用 $addToSet + $arrayElemAt 控制取值。
-
$push会保留所有值,内存占用高,大数据量慎用 -
$max/$min对字符串也有效,但按字典序,不是按业务含义 - 需要去重统计数量?用
$addToSet配合$size,别用$push+$size
复合分组字段顺序与索引是否匹配,决定查询能不能走索引
MongoDB 的分组管道本身不走索引,但如果前面有 $match 或后面接 $sort,索引就起作用了。关键在于:如果你的分组字段是 { category: 1, status: 1 },那最好建一个前缀匹配的索引,比如 { category: 1, status: 1, created_at: -1 }。
反例:只建了 { status: 1, category: 1 } 索引,而分组时用的是 { category: "$category", status: "$status" },这个顺序不一致,$sort 阶段很可能无法使用索引。
- 索引字段顺序必须和
_id对象中键的顺序严格一致才能高效支持后续排序 -
$group本身内存消耗大,单次分组文档数超 100MB 会报错,得加allowDiskUse: true - 聚合管道里越早过滤(
$match),分组压力越小;别把$match放在$group后面再筛











