我有一个庞大的数据集(数亿),其中包含以下在Mongo 7.0中运行的文档.文档要大得多,但这些都是与这个问题相关的字段.

{
 "groupId": "12345",
 "actionPerformed": "someAction",
 "date": "2023-08-17T18:16:58.000Z" // stored as ISO Dates
}

我想按groupId对它们进行分组,并获取每个组中执行的最新操作.

我在date字段上聚合到$sort,然后在组阶段使用$first,还是在组阶段使用$top聚合累加器为我进行排序更有效?这两个都在我的索引中,我只是找不到关于这个主题的任何关于优化的文档.

推荐答案

date场上使用$sort的第一种方法,然后在$group阶段使用$first.But使其发挥作用的关键是您需要使其成为复合排序.让我们go 探索吧!


设置和测试

作为参考,我们正在查看的两条管道是:

  1. $sort+$group,$first:
[
  { '$sort': { groupId: 1, date: -1 } },
  { '$group': { _id: '$groupId', doc: { '$first': '$$ROOT' } } }
]
  1. $group:$top:
[
  {
    '$group': {
      _id: '$groupId',
      doc: { '$top': { output: '$$ROOT', sortBy: { date: -1 } } }
    }
  }
]

请注意,我们对第一个管道使用的是复合排序,而不是第二个.将复合排序与第一个管道一起使用的动机将很快见分晓.在 compose 本文时,无论sortBy使用单一排序还是复合排序,此答案中描述的第二个管道的行为都是相同的.

现在,我们将创建两个复合索引(出于完整性考虑),以了解数据库在执行操作时如何利用它们.它们是:

  { groupId: 1, date: -1 }
  { date: -1, groupId: 1 }

判断操作效率的标准方法是判断关联的解释计划.我们通过运行db.foo.aggregate(<pipeline>).explain()次来收集这些数据.当我们查看第一个管道的输出时,我们可以看到winningPlan中嵌套了以下内容:

stage: 'DISTINCT_SCAN',
keyPattern: { groupId: 1, date: -1 },

相比之下,第二条管道的输出在winningPlan条管道中显示如下:

stage: 'COLLSCAN',

"Is it more efficient?"

正如我们在上面看到的,第一个管道能够使用索引来标识每个组的文档,而后一个管道 Select 执行完整的集合扫描.从最广泛的意义上讲,索引扫描比集合扫描更高效.推而广之,对于你提出的哪种方法更有效的问题,答案是第一条管道.

像这样的问题总是有细微差别的.在这种情况下,重要的是数据的基数,特别是这里的groupId字段.如果每个文档实际上都有一个唯一的groupId值,那么COLLSCAN计划实际上会更有效率.这是因为另一种计划需要扫描整个索引plus,以访问集合中的所有文档,这比刚开始扫描整个集合要做更多的工作.

groupId个值包含的文档越多,使用索引的第一个管道与执行集合扫描的第二个管道相比,效率提升就越大.


解释

那么,这里到底发生了什么?

索引是有序的数据 struct .对于复合指数{ groupId: 1, date: -1 },键将首先按groupId排序,然后再按date排序.通过扩展,数据库然后知道与每个groupId相关联的所有关键字将在索引中顺序定位.此外,它还知道,一旦它读取了与groupId相关联的键,则在包含下一个groupId的部分开始之前,在查询的索引中将没有其他感兴趣的东西.

这种对索引 struct 的了解使优化器能够构造利用该索引的DISTINCT_SCAN计划.但它只有在the index can satisfy the requested sort时才能这样做,这就是为什么我们不得不扩展它,使其成为复合排序,即使它在逻辑上不会影响结果.

Hypothetically第二条管道可以做类似的事情,但正如解释计划显示的那样,目前还没有实现这种(可能更棘手的)优化.因此,我们正在对第一个管道所做的工作是,通过给优化器一个"更简单"的请求来处理它,从而帮助 bootstrap 优化器实现一个更有效的计划.


支持material

官方文档中有几个地方提到了这种优化.其中一个是here,它总体上描述了这种方法:

如果管道按相同的字段进行排序和分组,而$group阶段只使用$first$last累加器运算符,请考虑在已分组的字段上添加与排序顺序匹配的索引.在某些情况下,$group阶段可以使用索引来快速查找每个组的第一个或最后一个文档.

另外,使用此优化的要求在this section of the documentation的"$group阶段"部分中概述:

如果stage同时满足这两个条件:

  • 管道按相同的字段进行排序和分组.
  • $group级只使用$first$last累加器运算符.

您可以使用this playground作为一个起点来进一步研究这个概念.

其他注意事项

在你的描述中,你提到了"these are the fields related to this question.",你包括了一个名为"actionPerformed"的字段,但你在问题的其他地方实际上从来没有提到过它.

如果您碰巧在聚合逻辑中也使用了该字段,比如过滤掉与"someAction"不匹配的文档,那么您可能还需要在索引定义中包含该字段.

Mongodb相关问答推荐

Mongo如何找到字段排除特殊字符

如何在MongoDB中对两个数组进行分组?

使用MongoDB 4将根文档替换为数组

MongoDB:从开始日期和结束日期数组中匹配特定日期的聚合查询

在我的查询中使用 populate() 时的 MongoDB createIndex()

在 MongoDB 中使用 findOneAndUpdate 有条件地更新/更新嵌入式数组

以聚合顺序使用 $$ROOT

_id 和 $oid 的区别; mongo 数据库中的 $date 和 IsoDate

将 MongoDB BsonDocument 转换为字符串

为什么使用整数作为 pymongo 的键不起作用?

Meteor 中的平均聚合查询

MongoDB 日志(log)文件和 oplog 有何不同?

从 id 删除 PyMongo 中的文档

指定在 mongodb .js 脚本中使用哪个数据库

如何使用 MongoDB C# 驱动程序有条件地组合过滤器?

一起使用 MongoDB 和 Neo4j

使用 Node.js 将许多记录插入 Mongodb 的正确方法

MongoDB InsertMany 与 BulkWrite

MongoDb - 利用多 CPU 服务器进行写入繁重的应用程序

mongoose查询返回 null