在date
场上使用$sort
的第一种方法,然后在$group
阶段使用$first
.But使其发挥作用的关键是您需要使其成为复合排序.让我们go 探索吧!
设置和测试
作为参考,我们正在查看的两条管道是:
$sort
+$group
,$first
:
[
{ '$sort': { groupId: 1, date: -1 } },
{ '$group': { _id: '$groupId', doc: { '$first': '$$ROOT' } } }
]
$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"
不匹配的文档,那么您可能还需要在索引定义中包含该字段.