MongoDB-组后对内部数组进行安全排序

时间:2019-09-23 14:51:17

标签: mongodb aggregation-framework

我正在尝试查找符合特定条件的所有记录,在这种情况下,_id是特定值,然后仅返回前2个结果(按名称字段排序)。

这就是我所拥有的

db.getCollection('col1').aggregate([
    {$match: {fk: {$in: [1, 2]}}},
    {$sort: {fk: 1, name: -1}},
    {$group: {_id: "$fk", items: {$push: "$$ROOT"} }},
    {$project: {items: {$slice: ["$items", 2]} }}
])

它可以工作,但是,不能保证。根据{{​​3}},$group不保证文档顺序。

这也意味着此处和其他地方建议的所有解决方案,建议先使用$unwind,然后依次使用$sort$group,对于同样的方法原因。

用Mongo(任何版本)完成此操作的最佳方法是什么?我已经看到建议可以在$project阶段完成此操作的建议,但是我不太确定该怎么做。

3 个答案:

答案 0 :(得分:2)

您正确地说$group的结果从未排序。

  

$ group不订购其输出文档。

因此做一个;

{$sort: {fk: 1}}

然后与

分组
{$group: {_id: "$fk", ... }}, 

将是浪费的精力。


但是在$group阶段之前,name: -1会进行排序的银衬里。由于您使用的是$push(而不是$addToSet),因此插入的对象将保留其在items结果中新创建的$group数组中的顺序。您可以看到此行为here (copy of your pipeline)

items数组将总是 具有;

"items": [
  {
    ..
    "name": "Michael"
  },
  {
    ..
    "name": "George"
  }
]

以相同顺序,因此您的嵌套数组排序是非问题!尽管我无法在文档中找到确切的报价来确认此行为,但是您可以检查;

  • this
  • this确认。
  • 此外,accumulator operator list代表$group,其中$addToSet的描述中带有"Order of the array elements is undefined.",而类似的运算符$push没有,这可能是间接的证据? :)

只需对您的管道进行简单的修改,即可将fk: 1的类别从前$group阶段移至后$group阶段;

db.getCollection('col1').aggregate([
    {$match: {fk: {$in: [1, 2]}}},
    {$sort: {name: -1}},
    {$group: {_id: "$fk", items: {$push: "$$ROOT"} }},
    {$sort: {_id: 1}},
    {$project: {items: {$slice: ["$items", 2]} }}
])

应该足以固定主要结果数组的顺序。在mongoplayground

上检查

答案 1 :(得分:1)

$ group不能保证文档顺序,但是可以将分组文档按每个存储区的排序顺序保留。因此,在您的情况下,即使$ group阶段之后的文档未按fk排序,但每个组(项目)也将按名称降序排序。如果您想按fk排序文档,只需在$ group阶段之后添加{$sort:{fk:1}}

如果需要,您还可以根据匹配查询中传递的值的顺序进行排序,方法是为每个文档添加一个额外的字段。

db.getCollection('col1').aggregate([
    {$match: {fk: {$in: [1, 2]}}},
    {$addField:{ifk:{$indexOfArray:[[1, 2],"$fk"]}}},
    {$sort: {ifk: 1, name: -1}},
    {$group: {_id: "$ifk", items: {$push: "$$ROOT"}}},
    {$sort: {_id : 1}},
    {$project: {items: {$slice: ["$items", 2]}}}
])

更新以允许在没有组运算符的情况下进行数组排序:我发现jira将允许对数组进行排序。

您可以在$project以下阶段尝试对数组进行排序。也许有多种方法可以执行此操作。这应该将名称降序排列。工作正常,但解决方案较慢。

{"$project":{"items":{"$reduce":{
  "input":"$items",
  "initialValue":[],
  "in":{"$let":{
    "vars":{"othis":"$$this","ovalue":"$$value"},
    "in":{"$let":{
      "vars":{
        //return index as 0 when comparing the first value with initial value (empty) or else return the index of value from the accumlator array which is closest and less than the current value.
        "index":{"$cond":{
          "if":{"$eq":["$$ovalue",[]]},
          "then":0,
          "else":{"$reduce":{
            "input":"$$ovalue",
            "initialValue":0,
            "in":{"$cond":{
              "if":{"$lt":["$$othis.name","$$this.name"]},
              "then":{"$add":["$$value",1]},
              "else":"$$value"}}}}
        }}
      },
      //insert the current value at the found index
      "in":{"$concatArrays":[
          {"$slice":["$$ovalue","$$index"]},
          ["$$othis"],
          {"$slice":["$$ovalue",{"$subtract":["$$index",{"$size":"$$ovalue"}]}]}]}
    }}}}
}}}}

一个简单的示例,演示每个迭代的工作原理

db.b.insert({"items":[2,5,4,7,6,3]});

othis   ovalue      index      concat arrays (parts with counts)       return value
2       []          0           [],0            [2]     [],0           [2]
5       [2]         0           [],0            [5]     [2],-1         [5,2]          
4       [5,2]       1           [5],1           [4]     [2],-1         [5,4,2]
7       [5,4,2]     0           [],0            [7]     [5,4,2],-3     [7,5,4,2]
6       [7,5,4,2]   1           [7],1           [6]     [5,4,2],-3     [7,6,5,4,2]
3       [7,6,5,4,2] 4           [7,6,5,4],4     [3]     [2],-1         [7,6,5,4,3,2]

参考-Sorting Array with JavaScript reduce function

答案 2 :(得分:1)

该问题中有一个红色的鲱鱼,因为$group 确实保证将按顺序处理传入的文档(这就是为什么您必须在$group之前对它们进行排序才能获得有序的数组),但是您建议这样做的方式存在问题,因为将所有文档归为一个单一的组是(a)低效的,并且(b)可能超过最大文档大小。

由于您只想要前两个,所以对于每个唯一的fk值,最有效的方法是通过使用$lookup的“子查询”,如下所示:

db.coll.aggregate([
 {$match: {fk: {$in: [1, 2]}}},
 {$group:{_id:"$fk"}}, 
 {$sort: {_id: 1}},
 {$lookup:{
      from:"coll", 
      as:"items", 
      let:{fk:"$_id"},
      pipeline:[ 
           {$match:{$expr:{$eq:["$fk","$$fk"]}}}, 
           {$sort:{name:-1}},
           {$limit:2}, 
           {$project:{_id:0, fk:1, name:1}}
      ]
 }}
])

假设您必须在{fk:1, name:-1}上有一个索引,才能在提议的代码中进行有效的排序,此处的前两个阶段将通过DISTINCT_SCAN计划使用该索引,这非常有效,并且对于每个其中$lookup将使用相同的索引按fk的单个值进行过滤,并返回已排序并限制在前两个中的结果。至少在服务器实现https://jira.mongodb.org/browse/SERVER-9377之前,这将是最有效的方法。