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

时间:2015-08-18 06:45:38

标签: mongodb aggregation-framework

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

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

{name: "Person1", marks: 20}
{name: "Person2", marks: 20}
{name: "Person1", marks: 20}

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

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

我有以下问题。

  1. 排序顺序在最终结果中丢失。如果我在$ group之后放置另一种排序,则结果会正确排序。这是否意味着$ group不维护以前的排序顺序?
  2. 我想将结果限制为5.在传递到限制之前,是否必须完全执行组操作(对于所有1000条记录)。 (或)组操作在记录时将记录传递到限制阶段,并在满足限制阶段的要求时停止处理?
  3. 我的实际想法是对聚合结果进行分页。在上面的场景中,如果$ group维护排序顺序并仅处理所需的记录数,我想在后续页面查询中应用$match condition {$ge: 'lastPersonName'}

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

4 个答案:

答案 0 :(得分:6)

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

这是一个常见的unix习语:

ps -ef | grep mongod | tee "out.txt"

在这种情况下,第一个命令ps -ef的输出被“管道”到下一个命令grep mongod,而命令tee out.txt又输出“管道”到db.classtest.aggregate([ { "$group": { "_id": "$name", "total": { "$sum": "$marks"} }}, { "$sort": { "name": 1 } }, { "$limit": 5 } ]) 输出到终端以及指定的文件名。这是一个“管道”,每个阶段“馈送”下一个阶段,并按照它们所写的序列的“顺序”。

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

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

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

然后你会根据需要$sort结果和$limit

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

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

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

类似的东西:

TSimpleServer

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

答案 1 :(得分:4)

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

正如其他人指出的那样:

  1. $group不保留顺序,因此早期排序没有多大帮助。
  2. $group不进行任何优化,即使有以下$limit,即在整个集合上运行$group
  3. 我的用例具有以下独特功能,这有助于我解决它:

    1. 每位学生最多会有10条记录(最少1条)。
    2. 我对页面大小不太了解。前端能够处理不同的页面大小。 以下是我使用的聚合命令。

      db.classtest.aggregate(
      [
          {$sort: {name: 1}},
          {$limit: 5 * 10},
          {$group: {_id: '$name',
              total: {$sum: '$marks'}}},
          {$sort: {_id: 1}}
      ])
      
    3. 解释上述内容。

      1. 如果$sort紧接在$limit之前,框架会优化要发送到下一阶段的数据量。请参阅here
      2. 要获得至少5条记录(页面大小),我需要传递至少5(页面大小)* 10(每位学生的最大记录数)= 50条记录到$group阶段。这样,最终结果的大小可以是0到50之间的任何值。
      3. 如果结果小于5,则不需要进一步分页。
      4. 如果结果大小超过5,可能有可能没有完全处理最后一个学生记录(即没有对学生的所有记录进行分组),因此我从结果中丢弃了最后一条记录。 /强>
      5. 然后,在后续页面请求中将最后一条记录中的名称(保留结果中的名称)用作$匹配条件,如下所示。

        db.classtest.aggregate(
        [
            {$match: {name: {$gt: lastRecordName}}}
            {$sort: {name: 1}},
            {$limit: 5 * 10},
            {$group: {_id: '$name',
                total: {$sum: '$marks'}}},
            {$sort: {_id: 1}}
        ])
        
      6. 在上面,框架仍将优化$match, $sort and $limit作为单一操作,我已通过解释计划确认。

答案 2 :(得分:2)

  1. $group 订购其输出文档。”请参阅http://docs.mongodb.org/manual/reference/operator/aggregation/group/

  2. $limit限制前一个$sort操作的已处理元素的数量,而不仅仅是传递给下一个阶段的元素数量。请参阅http://docs.mongodb.org/manual/reference/operator/aggregation/limit/

  3. 上的说明

    对于您提出的第一个问题,我不确定,但看起来(见1.)阶段n + 1可以影响阶段n的行为:限制将限制对前n个元素进行排序操作,排序操作不会完成,就像下面的限制阶段不存在一样。

答案 3 :(得分:1)

对组数据mongodb的分页-

在$ group项目中,您不能直接应用分页,但将使用以下技巧,

如果要在组数据上分页-

例如-我希望将产品分组为category,然后每个类别只希望有5种产品

步骤1-在产品表上写汇总,并写groupBY

        { $group: { _id: '$prdCategoryId', products: { $push: '$$ROOT' } } },

步骤2-跳过prdSkip,限制数据的限制,将其传递 动态

        {
            $project: {
                // pagination for products
                products: {
                    $slice: ['$products', prdSkip, prdLimit],
                }
            }
        },

最终查询看起来像- 参数-限制,跳过-用于类别分页 和prdSkip和PrdLimit进行产品分页

    db.products.aggregate([

        { $group: { _id: '$prdCategoryId', products: { $push: '$$ROOT' } } },
        {
            $lookup: {
                from: 'categories',
                localField: '_id',
                foreignField: '_id',
                as: 'categoryProducts',
            },
        },
        {
            $replaceRoot: {
                newRoot: {
                    $mergeObjects: [{ $arrayElemAt: ['$categoryProducts', 0] }, '$$ROOT'],
                },
            },
        },
        {
            $project: {
                // pagination for products
                products: {
                    $slice: ['$products', prdSkip, prdLimit],
                },
                _id: 1,
                catName: 1,
                catDescription: 1,
            },
        },
    ])
    .limit(limit) // pagination for category
    .skip(skip);

我在这里使用replaceRoot来提取类别。