限制 MongoDB 聚合操作中推送到数组的对象数量。

4

我一直在寻找一种方法来限制在使用 MongoDB 集合上的“聚合”时,我正在创建数组并将对象推送到数组中的数量。

我有一个学生集合 - 每个学生都有这些相关键: 本学期所选课程号(仅一个值), 班级百分位数(如果已注册班级则存在,否则为 null), 当前课程成绩(>0 如果已注册班级,否则为 0), 整体平均成绩(GPA), 最高成绩。

我需要将所有从未失败过的学生按班级分组,并将具有高于 80 的 GPA 的学生放入一个数组中,另一个数组包含这些 GPA 不存在的学生,并按照其特定班级的成绩进行排序。

这是我的查询:

db.getCollection("students").aggregate([
{"$match": {
    "class_number": 
        {"$in": [49, 50, 16]},
    "grades.curr_class.percentile": 
        {"$exists": true},
    "grades.min": {"$gte": 80},
    }},
    {"$sort": {"grades.curr_class.score": -1}},
    {"$group": {"_id": "$class_number", 
                "studentsWithHighGPA": 
                    {"$push": 
                        {"$cond": [{"$gte": ["$grades.gpa", 80]},
                        {"id": "$_id"},
                        "$$REMOVE"]
                        }
                    },
                 "studentsWithoutHighGPA":
                 {"$push": 
                        {"$cond": [{"$lt": ["$grades.gpa", 80]},
                        {"id": "$_id"},
                        "$$REMOVE"]
                        }, 
                    },
                    }, 
                },
])

我想做的是限制每个数组中学生的数量。我只想要每个数组中的前16名,但我不知道如何处理。
提前感谢您的回复!
我已经尝试使用不同变量的限制和切片,但似乎都没有起作用。
3个回答

3

自mongoDb 5.0版本以来,一种选择是使用$setWindowFields,特别是它的$rank选项。这将允许仅保留相关学生并在$group步骤之前限制其数量:

  1. $match只匹配OP建议的相关学生
  2. $set setWindowFieldsgroupId(因为它目前只能按一个键进行分区)
  3. $setWindowFields定义每个学生在其数组中的排名
  4. $match只匹配所需排名的学生
  5. $group按OP建议的class_number进行分组:
db.collection.aggregate([
  {$match: { 
     class_number: {$in: [49, 50, 16]},
      "grades.curr_class.percentile": {$exists: true},
      "grades.min": {$gte: 80}
  }},
  {$set: {
      groupId: {$concat: [
          {$toString: "$class_number"},
          {$toString: {$toBool: {$gte: ["$grades.gpa", 80]}}}
      ]}
  }},
  {$setWindowFields: {
      partitionBy: "$groupId",
      sortBy: {"grades.curr_class.score": -1},
      output: {rank: {$rank: {}}}
  }},
  {$match: {rank: {$lte: rankLimit}}},
  {$group: {
      _id: "$class_number",
      studentsWithHighGPA: {$push: {
          $cond: [{$gte: ["$grades.gpa", 80]}, {id: "$_id"}, "$$REMOVE"]}},
      studentsWithoutHighGPA: {$push: {
          $cond: [{$lt: ["$grades.gpa", 80]}, {id: "$_id"}, "$$REMOVE"]}}
  }}
])

playground example上看看它是如何工作的。

*这个解决方案将限制学生的排名,因此在数组中有多于n个学生的情况下会出现边缘情况(如果有多个学生的排名恰好为n)。可以通过添加$slice步骤来简单地解决它。


2
也许 MongoDB 的 $facets 是一个解决方案。你可以在一个聚合调用中指定不同的输出管道。
就像这样:
const pipeline = [
    {
        '$facet': {
            'studentsWithHighGPA': [
                { '$match': { 'grade': { '$gte': 80 } } }, 
                { '$sort': { 'grade': -1 } }, 
                { '$limit': 16 }
            ], 
            'studentsWithoutHighGPA': [
                { '$match': { 'grade': { '$lt': 80 } } },
                { '$sort': { 'grade': -1 } },
                {  '$limit': 16 }
            ]
        }
    }
];

    
coll.aggregate(pipeline)

这应该最终生成一个包含两个数组的文档。

studentsWithHighGPA (array)
    0 (object)
    1 (object)
    ...
studentsWithoutHighGPA (array)
    0 (object)
    1 (object)

将每个方面视为自己的聚合管道。因此,您还可以包括$group按类别或其他方式进行分组。

https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/


这种方法有两个主要的缺点:1. $facet 不使用索引。2. 这将把所有文档分组成一个大文档,而文档有一个大小限制,这可能会导致查询失败。 - nimrod serok

1

我认为在$group阶段内,mongodb没有提供限制数量的操作符。

您可以使用$accumulator,但这需要启用服务器端脚本,并可能会影响性能。

如果要在整个分组过程中将studentsWithHighGPA限制为16,则可能如下所示:

      "studentsWithHighGPA": {
        "$accumulator": {
          init: "function(){
                     return {combined:[]};
          }",
          accumulate: "function(state, id, score){
                              if (score >= 80) {
                                    state.combined.push({_id:id, score:score})
                              };
                              return {combined:state.combined.slice(0,16)}
          }",
          accumulateArgs: [ "$_id", "$grades.gpa"],
          merge: "function(A,B){
                     return {combined: 
                              A.combined.concat(B.combined).sort(
                                    function(SA,SB){
                                           return (SB.score - SA.score)
                                    })
                            }
          }",
          finalize: "function(s){
               return s.combined.slice(0,16).map(function(A){
                  return {_id:A._id}
               })
          }",
          lang: "js"
        }
      }

请注意,分数也一直保留到最后,以便可以正确地组合来自不同碎片的部分结果集。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接