定义
兼容性
可以使用 $group 查找托管在以下环境中的部署:
- MongoDB Atlas:用于云中 MongoDB 部署的完全托管服务 
- MongoDB Enterprise:基于订阅、自我管理的 MongoDB 版本 
- MongoDB Community:源代码可用、免费使用且可自行管理的 MongoDB 版本 
语法
$group 阶段具有以下原型形式:
{  $group:    {      _id: <expression>, // Group key      <field1>: { <accumulator1> : <expression1> },      ...    }  } 
| 字段 | 说明 | 
|---|---|
| 
 | 必需。 | 
| 
 | 
Considerations
性能
$group 是一个阻塞阶段,这会导致管道在处理数据前等待为阻塞阶段检索所有输入数据。阻塞阶段可能会降低性能,因为它会减少具有多个阶段的管道的并行处理。对于大型数据集,阻塞阶段还可能使用大量内存。
累加器操作符
<accumulator> 操作符必须是以下累加器操作符之一:
| 名称 | 说明 | 
|---|---|
| 返回用户定义的累加器函数的结果。 | |
| 返回每个群组的唯一表达式值数组。未定义数组元素的排序。 5.0 版中的更改:可在  | |
| 返回数值的平均值。忽略非数字值。 5.0 版中的更改:可在  | |
| 返回组合了两个或多个数组元素的单个大量。 8.1版本新增。 | |
| 返回群组中第一个文档的表达式结果。 5.0 版中的更改:可在  | |
| 返回群组内前  5.2 版新增功能:可在  | |
| 返回群组中最后一份文档的表达式结果。 5.0 版中的更改:可在  | |
| 返回群组内后  5.2 版新增功能:可在  | |
| 返回每个群组的最大表达式值。 5.0 版中的更改:可在  | |
| 返回通过组合每个组的输入文档创建的文档。 | |
| 返回每个群组的最小表达式值。 5.0 版中的更改:可在  | |
| 返回每组中文档的大量表达式值。 5.0 版中的更改:可在  | |
| 接受两个或多个数组,并返回一个数组,其中包含出现在每个输入数组中的元素。 8.1版本新增。 | |
| 返回输入值的总体标准偏差。 5.0 版中的更改:可在  | |
| 返回输入值的样本标准偏差。 5.0 版中的更改:可在  | |
| 返回数值的总和。忽略非数字值。 5.0 版中的更改:可在  | |
$group 和内存限制
如果 $group 阶段超过 100 兆字节 RAM,MongoDB 会将数据写入临时文件。但是,如果将 allowDiskUse 选项设置为 false,$group 将返回错误。有关更多信息,请参阅聚合管道限制。
$group 性能优化
本节将介绍为提高 $group 性能而进行的优化。有些优化可以手动执行,有些优化由 MongoDB 在内部执行。
优化以返回每个群组的第一份或最后一份文档
如果同一字段和 $group 阶段的管道 sorts 和 groups 仅使用 $first 或 $last 累加器操作符,请考虑在分组字段上添加与排序顺序匹配的索引。在某些情况下,$group 阶段可以使用索引来快速找到每组的第一个文档。
例子
如果名为 foo 的集合包含索引 { x: 1, y: 1 },则以下管道可以使用该索引来查找每个组的第一个文档:
db.foo.aggregate([   {     $sort:{ x : 1, y : 1 }   },   {     $group: {       _id: { x : "$x" },       y: { $first : "$y" }     }   } ]) 
请参阅:基于插槽的查询执行引擎
从版本 5.2 开始,如果满足以下任一条件,MongoDB 将使用基于插槽的执行查询引擎来执行 $group 阶段:
- $group是管道中的第一个阶段。
- 管道中的所有先前阶段也可以由基于槽位的执行引擎执行。 
有关更多信息,请参阅 $group 优化。
示例
按字段对文档进行分组和计数
在 mongosh 中创建名为 sales 的示例集合,其中包含以下文档:
db.sales.insertMany([   { "_id" : 1, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("2"), "date" : ISODate("2014-03-01T08:00:00Z") },   { "_id" : 2, "item" : "jkl", "price" : Decimal128("20"), "quantity" : Int32("1"), "date" : ISODate("2014-03-01T09:00:00Z") },   { "_id" : 3, "item" : "xyz", "price" : Decimal128("5"), "quantity" : Int32( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },   { "_id" : 4, "item" : "xyz", "price" : Decimal128("5"), "quantity" :  Int32("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },   { "_id" : 5, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },   { "_id" : 6, "item" : "def", "price" : Decimal128("7.5"), "quantity": Int32("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },   { "_id" : 7, "item" : "def", "price" : Decimal128("7.5"), "quantity": Int32("10") , "date" : ISODate("2015-09-10T08:43:00Z") },   { "_id" : 8, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") }, ]) 
以下聚合操作使用$group sales阶段按price 字段对 集合中的文档群组。一群组包含价格大于或等于10 的商品。第二群组包含价格小于10 的商品数量。然后,管道计算群组中的文档数量。
db.sales.aggregate(  [    {       $group: {           _id: {             $cond: {               if: { $gte: [ "$price", Decimal128("10") ] },               then: "Price >= 10",               else: "Price < 10"             }           },           count: { $sum: 1 }       }    } ]  ) 
操作返回以下结果:
{ _id: 'Price >= 10', count: 4 }, { _id: 'Price < 10', count: 4 } 
这个聚合操作相当于以下 SQL 语句:
SELECT    CASE       WHEN price >= 10 THEN 'Price >= 10'       ELSE 'Price < 10'    END AS price_group,    COUNT(*) AS count FROM   sales GROUP BY   price_group; 
Retrieve Distinct Values
以下聚合操作使用 $group 阶段检索 sales 集合中不同的项目值:
db.sales.aggregate( [ { $group : { _id : "$item" } } ] ) 
操作返回以下结果:
{ "_id" : "abc" } { "_id" : "jkl" } { "_id" : "def" } { "_id" : "xyz" } 
注意
示例,以下形式的 $group 操作可能会产生 DISTINCT_SCAN:
{ $group : { _id : "$<field>" } } 
要查看操作是否会产生DISTINCT_SCAN ,请检查操作的解释结果。
按列项分组有
下面的聚合操作按 item 字段对文档进行分组,计算每个列项的总销售额,然后只返回总销售额大于或等于 100 的项目:
db.sales.aggregate(   [     // First Stage     {       $group :         {           _id : "$item",           totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } }         }      },      // Second Stage      {        $match: { "totalSaleAmount": { $gte: 100 } }      }    ]  ) 
- 第一个阶段:
- $group阶段按- item对文档进行分组,以检索非重复的项值。此阶段返回每一项的- totalSaleAmount。
- 第二个阶段:
- $match阶段会对生成的文档进行筛选,从而只返回- totalSaleAmount大于或等于 100 的项目。
操作返回以下结果:
{ "_id" : "abc", "totalSaleAmount" : Decimal128("170") } { "_id" : "xyz", "totalSaleAmount" : Decimal128("150") } { "_id" : "def", "totalSaleAmount" : Decimal128("112.5") } 
这个聚合操作相当于以下 SQL 语句:
SELECT item,    Sum(( price * quantity )) AS totalSaleAmount FROM   sales GROUP  BY item HAVING totalSaleAmount >= 100 
提示
计算数量、总和和平均值
在 mongosh 中创建名为 sales 的示例集合,其中包含以下文档:
db.sales.insertMany([   { "_id" : 1, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("2"), "date" : ISODate("2014-03-01T08:00:00Z") },   { "_id" : 2, "item" : "jkl", "price" : Decimal128("20"), "quantity" : Int32("1"), "date" : ISODate("2014-03-01T09:00:00Z") },   { "_id" : 3, "item" : "xyz", "price" : Decimal128("5"), "quantity" : Int32( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },   { "_id" : 4, "item" : "xyz", "price" : Decimal128("5"), "quantity" :  Int32("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },   { "_id" : 5, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },   { "_id" : 6, "item" : "def", "price" : Decimal128("7.5"), "quantity": Int32("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },   { "_id" : 7, "item" : "def", "price" : Decimal128("7.5"), "quantity": Int32("10") , "date" : ISODate("2015-09-10T08:43:00Z") },   { "_id" : 8, "item" : "abc", "price" : Decimal128("10"), "quantity" : Int32("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") }, ]) 
按当年天数分组
以下管道计算 2014 年每一天的总销售额、平均销售数量和销售数量:
db.sales.aggregate([   // First Stage   {     $match : { "date": { $gte: new ISODate("2014-01-01"), $lt: new ISODate("2015-01-01") } }   },   // Second Stage   {     $group : {        _id : { $dateToString: { format: "%Y-%m-%d", date: "$date" } },        totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } },        averageQuantity: { $avg: "$quantity" },        count: { $sum: 1 }     }   },   // Third Stage   {     $sort : { totalSaleAmount: -1 }   }  ]) 
- 第一个阶段:
- $match阶段会对这些文档进行筛选,已仅将从 2014 年开始的文档传递到下一阶段。
- 第二个阶段:
- $group阶段按日期对文档分组,并计算每组文档的总销售金额、平均数量和总数。
- 第三个阶段:
- $sort阶段按每个组的总销售金额对结果进行降序排序。
操作返回以下结果:
{    "_id" : "2014-04-04",    "totalSaleAmount" : Decimal128("200"),    "averageQuantity" : 15, "count" : 2 } {    "_id" : "2014-03-15",    "totalSaleAmount" : Decimal128("50"),    "averageQuantity" : 10, "count" : 1 } {    "_id" : "2014-03-01",    "totalSaleAmount" : Decimal128("40"),    "averageQuantity" : 1.5, "count" : 2 } 
这个聚合操作相当于以下 SQL 语句:
SELECT date,        Sum(( price * quantity )) AS totalSaleAmount,        Avg(quantity)             AS averageQuantity,        Count(*)                  AS Count FROM   sales WHERE  date >= '01/01/2014' AND date < '01/01/2015' GROUP  BY date ORDER  BY totalSaleAmount DESC 
提示
- db.collection.countDocuments(),使用- $sum表达式包装- $group聚合阶段。
分组方式: null
下面的聚合操作指定了 null 的 _id 组,计算集合中所有文档的总销售额、平均数量和计数。
db.sales.aggregate([   {     $group : {        _id : null,        totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } },        averageQuantity: { $avg: "$quantity" },        count: { $sum: 1 }     }   }  ]) 
操作返回以下结果:
{   "_id" : null,   "totalSaleAmount" : Decimal128("452.5"),   "averageQuantity" : 7.875,   "count" : 8 } 
这个聚合操作相当于以下 SQL 语句:
SELECT Sum(price * quantity) AS totalSaleAmount,        Avg(quantity)         AS averageQuantity,        Count(*)              AS Count FROM   sales 
提示
- db.collection.countDocuments(),使用- $sum表达式包装- $group聚合阶段。
Pivot Data
在 mongosh 中创建名为 books 的示例集合,其中包含以下文档:
db.books.insertMany([   { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 },   { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 },   { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 },   { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 },   { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 } ]) 
将 title 按 author 进行分组
以下聚合操作将books集合中的数据转换为按作者分组的标题。
db.books.aggregate([    { $group : { _id : "$author", books: { $push: "$title" } } }  ]) 
该操作将返回以下文档:
{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] } { "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] } 
文档分组依据 author
以下聚合操作按 author 对文档进行分组:
db.books.aggregate([    // First Stage    {      $group : { _id : "$author", books: { $push: "$$ROOT" } }    },    // Second Stage    {      $addFields:        {          totalCopies : { $sum: "$books.copies" }        }    }  ]) 
- 第一个阶段:
- $group使用- $$ROOT系统变量将整个文档按作者分组。该阶段将以下文档传递到下一阶段:- { "_id" : "Homer", - "books" : - [ - { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 }, - { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 } - ] - }, - { "_id" : "Dante", - "books" : - [ - { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 }, - { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 }, - { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 } - ] - } 
- 第二个阶段:
- $addFields向输出添加一个字段,其中包含每位作者的图书总份数。- 注意- 生成的文档不得超过 16 MiB 的 BSON 文档大小限制。 - 该操作将返回以下文档: - { - "_id" : "Homer", - "books" : - [ - { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 }, - { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 } - ], - "totalCopies" : 20 - } - { - "_id" : "Dante", - "books" : - [ - { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 }, - { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 }, - { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 } - ], - "totalCopies" : 5 - } 
本页上的C#示例使用Atlas示例数据集中的 sample_mflix数据库。要学习;了解如何创建免费的MongoDB Atlas 群集并加载示例数据集,请参阅MongoDB .NET/ C#驱动程序文档中的入门。
以下 Movie 类对 sample_mflix.movies 集合中的文档进行建模:
public class Movie {     public ObjectId Id { get; set; }     public int Runtime { get; set; }          public string Title { get; set; }     public string Rated { get; set; }     public List<string> Genres { get; set; }     public string Plot { get; set; }          public ImdbData Imdb { get; set; }     public int Year { get; set; }     public int Index { get; set; }          public string[] Comments { get; set; }         []     public DateTime LastUpdated { get; set; } } 
注意
用于 Pascal Case 的 ConventionPack
此页面上的 C# 类在其属性名称中使用 Pascal 命名法,而 MongoDB 集合中的字段名称则使用 camel 命名法。为了解决这种差异,可以在应用程序启动时使用以下代码注册一个 ConventionPack:
var camelCaseConvention = new ConventionPack { new CamelCaseElementNameConvention() }; ConventionRegistry.Register("CamelCase", camelCaseConvention, type => true); 
要使用MongoDB .NET/C#驱动程序将 $group 阶段添加到聚合管道,请对 PipelineDefinition 对象调用 Group() 方法。
以下示例创建了一个管道阶段,该阶段按文档的 Rated字段的值对文档进行分组。每个群组的评分显示在每个输出文档中名为 Rating 的字段中。每个输出文档还包含一个名为 TotalRuntime 的字段,其值是该群组中所有电影的总运行时间。
var pipeline = new EmptyPipelineDefinition<Movie>()     .Group(         id: m => m.Rated,         group: g => new         {             Rating = g.Key,             TotalRuntime = g.Sum(m => m.Runtime)         }     ); 
本页上的 Node.js 示例使用 Atlas 示例数据集中的 sample_mflix数据库。要学习如何创建免费的MongoDB Atlas 集群并加载示例数据集,请参阅MongoDB Node.js驱动程序文档中的入门。
要使用MongoDB Node.js驱动程序将 $group 阶段添加到聚合管道,请在管道对象中使用 $group操作符。
以下示例创建了一个管道阶段,该阶段按文档的 rated字段的值对文档进行分组。每个输出文档都包含一个 rating字段,用于存储每个群组的评分。每个输出文档还包含一个名为 totalRuntime 的字段,用于存储该群组中所有电影的总运行时间。然后,该示例运行聚合管道:
const pipeline = [   {     $group: {       _id: "$rated",       rating: { $first: "$rated" },       totalRuntime: { $sum: "$runtime" }     }   } ]; const cursor = collection.aggregate(pipeline); return cursor; 
了解详情
Group and Total Data 教程提供了常见使用案例中 $group 操作符的广泛示例。
要学习;了解有关相关管道阶段的更多信息,请参阅 $addFields 指南。