Mongodb聚合$ group后跟$限制分页

Poo*_*rna 4 mongodb aggregation-framework

在MongoDB聚合管道中,从一个阶段到另一个阶段的记录流发生一次/批次(或)将等待当前阶段完成整个集合,然后再将其传递到下一阶段?

例如,我有一个带有以下样本记录的集合classtest

{name: "Person1", marks: 20}
{name: "Person2", marks: 20}
{name: "Person1", marks: 20}
Run Code Online (Sandbox Code Playgroud)

我有大约100名学生的1000条记录,我有以下聚合查询

    db.classtest.aggregate(
[
    {$sort: {name: 1}},
    {$group: {_id: '$name',
            total: {$sum: '$marks'}}},
    {$limit: 5}
])
Run Code Online (Sandbox Code Playgroud)

我有以下问题.

  1. 排序顺序在最终结果中丢失.如果我在$ group之后放置另一种排序,则结果会正确排序.这是否意味着$ group不维护以前的排序顺序?
  2. 我想将结果限制为5.在传递到限制之前,必须完成组操作(对于所有1000条记录).(或)当有记录时,组操作将记录传递到限制阶段,当满足限制阶段的要求时,停止处理?

我的实际想法是对聚合结果进行分页.在上面的场景中,如果$ group维护排序顺序并仅处理所需的记录数,我想$match condition {$ge: 'lastPersonName'}在后续页面查询中应用.

  1. 我不想在$ group之前应用$ limit,因为我想要5名学生的结果而不是前5个记录.
  2. 我可能不想使用$ skip,因为这意味着有效地遍历那些记录.

Poo*_*rna 9

我已经解决了这个问题,不需要维护另一个集合,甚至不需要 $group 遍历整个集合,因此发布了我自己的答案。

正如其他人指出的那样:

  1. $group 不保留顺序,因此早期排序没有多大帮助。
  2. $group不做任何优化,即使有后续$limit,即$group在整个集合上运行。

我的用例具有以下独特的功能,这帮助我解决了它:

  1. 每个学生最多有 10 条记录(最少 1 条)。
  2. 我对页面大小不是很特别。前端能够处理不同的页面大小。下面是我用过的聚合命令。

    db.classtest.aggregate(
    [
        {$sort: {name: 1}},
        {$limit: 5 * 10},
        {$group: {_id: '$name',
            total: {$sum: '$marks'}}},
        {$sort: {_id: 1}}
    ])
    
    Run Code Online (Sandbox Code Playgroud)

以上解释。

  1. 如果$sort紧接在 之前$limit,框架会优化要发送到下一阶段的数据量。参考这里
  2. 要获得最少 5 条记录(页面大小),我需要将至少 5(页面大小)* 10(每个学生的最大记录数)= 50 条记录传递到$group舞台。这样,最终结果的大小可能介于 0 到 50 之间。
  3. 如果结果小于 5,则不需要进一步分页。
  4. 如果结果大小大于 5,可能最后一个学生记录没有被完全处理(即没有将学生的所有记录分组),因此我从结果中丢弃最后一个记录。
  5. 然后将最后一条记录中的名称(在保留结果中)用作后续页面请求中的 $match 标准,如下所示。

    db.classtest.aggregate(
    [
        {$match: {name: {$gt: lastRecordName}}}
        {$sort: {name: 1}},
        {$limit: 5 * 10},
        {$group: {_id: '$name',
            total: {$sum: '$marks'}}},
        {$sort: {_id: 1}}
    ])
    
    Run Code Online (Sandbox Code Playgroud)

在上面,框架仍然会$match, $sort and $limit作为单个操作一起优化,我已经通过解释计划确认了这一点。


Bla*_*ven 6

这里要考虑的第一件事是聚合框架与要应用的阶段的"管道"一起工作以获得结果.如果您熟悉在操作系统的"命令行"或"shell"上处理事物,那么您可能对"管道"或|操作员有一些经验.

这是一个常见的unix习语:

ps -ef | grep mongod | tee "out.txt"
Run Code Online (Sandbox Code Playgroud)

在这种情况下,这里的第一个命令的输出ps -ef被"管道"到下一个命令grep mongod,而下一个命令又将其输出"管道" tee out.txt输出到终端以及指定的文件名.这是一个"管道",每个阶段"馈送"下一个阶段,并按照它们所写入的序列的"顺序".

聚合管道也是如此.这里的"管道"实际上是一个"数组",它是在处理数据到结果时传递的有序指令集.

db.classtest.aggregate([
    { "$group": {
      "_id": "$name",
      "total": { "$sum": "$marks"}
    }},
    { "$sort": { "name": 1 } },
    { "$limit": 5 }
])  
Run Code Online (Sandbox Code Playgroud)

所以这里发生的是集合中的所有项目首先被处理$group以获得它们的总数.没有指定的"顺序"进行分组,因此预先排序数据没有多大意义.这样做也没有任何意义,因为你还没有进入你的后期阶段.

然后你会$sort得到结果,也可以$limit根据需要.

对于您的下一个"数据页面",您最理想$match的是找到找到的最后一个唯一名称,如下所示:

db.classtest.aggregate([
    { "$match": { "name": { "$gt": lastNameFound } }},
    { "$group": {
      "_id": "$name",
      "total": { "$sum": "$marks"}
    }},
    { "$sort": { "name": 1 } },
    { "$limit": 5 }
])  
Run Code Online (Sandbox Code Playgroud)

这不是最好的解决方案,但实际上并没有这种分组的替代方案.然而,每次迭代结束时,它会显着"更快".或者,如果您的数据允许,在每个聚合语句中存储所有unqiue名称(或从另一个集合中读取该名称)和"分页"以及"范围查询"可能是一个可行的选项.

就像是:

db.classtest.aggregate([
    { "$match": { "name": { "$gte": "Allan", "$lte": "David" } }},
    { "$group": {
      "_id": "$name",
      "total": { "$sum": "$marks"}
    }},
    { "$sort": { "name": 1 } },
])  
Run Code Online (Sandbox Code Playgroud)

不幸的是,没有"限制分组直到x结果"选项,所以除非你可以使用另一个列表,否则你基本上将你发送的每个聚合查询分组一切(并且可能每次都会逐渐变小).