【问题标题】:MongoDB - Safely sort inner array after groupMongoDB - 在组后安全地对内部数组进行排序
【发布时间】:2020-01-23 16:14:57
【问题描述】:

我正在尝试查找与特定条件匹配的所有记录,在本例中 _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]} }}
])

它有效,但不能保证。根据this Mongo thread$group不保证文件顺序。

这也意味着此处和其他地方的所有建议解决方案,即建议使用$unwind,然后是$sort,然后是$group,出于同样的原因也将不起作用。

使用 Mongo(任何版本)完成此任务的最佳方法是什么?我看到了可以在$project 阶段完成的建议,但我不太确定如何。

【问题讨论】:

  • 只是为了测试,是否可以包含一些示例数据? $group 之后的 $sort 也会起作用吗?
  • 好的,我找不到任何未在组中结束的解决方案。
  • 我不明白这个问题,嵌套的items 数组将始终使用name: -1 正确排序,因为$group 不保持结果对象的顺序,但$push将保留items 数组中name 字段的先前顺序。只需将 fk 排序从 pre-$group 阶段删除到 post-$group 阶段即可。与here 一样,items 数组将始终具有正确的顺序,即使主要结果集的顺序是随机的。您可以按照我的建议强制执行,例如 here
  • 我认为您可能对那个文档点感到困惑。虽然$group 确实不能保证生成的“分组”文档的任何输出顺序,但当您按照自己的方式做某事时,$sort before 实际上是 $group 然后 项目将以相同的排序顺序添加到数组中。这自然是保证,否则实现$sort 将毫无意义。
  • 所以{ "a": 1, "b": 2 }, { "a": 1, "b": 1 }, { "a": 2, "b": 3 } 确实会可能 收集,以便_id: "$a" 分组键的顺序可能不同,即“分组”中的 2 在 1 之前,但如果你做了类似[{ "$sort": { "a": 1, "b": 1 }},{ "$group": { "_id": "$a", "list": { "$push": "$b" } } }] 然后list 按排序顺序总是,即使_id 键值不是。在最初应用 $sort 之后,没有什么会导致“推送”数组元素处于任何意外顺序。

标签: mongodb aggregation-framework


【解决方案1】:

你说$group 的结果永远不会排序是正确的。

$group 不会对其输出文档进行排序。

因此做一个;

{$sort: {fk: 1}}

然后分组

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

将是白费力气。


但是在$group 阶段和name: -1 之前有一个一线希望。由于您使用的是$push(不是$addToSet),插入的对象将保留它们在$group 结果中新创建的items 数组中的顺序。你可以看到这个行为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查看它

【讨论】:

  • 很好的答案,这很有意义!因此,似乎 $group 保证以相同的顺序 PROCESS INPUT 文档,这也将保证推送的数组有正确的顺序;只是输出组的顺序是不确定的。如果您能在某处找到记录在案的内容,那将是一笔巨大的奖励,否则我想我很高兴。
  • @AdamRackis 我很高兴,但我找不到$group 阶段的有序过程的确切细节,但您可以从$first$last 等运算符中猜到这种情况,对于那些工作,$group 操作必须尊重前一阶段的顺序,不是吗(或者可能仅适用于某些操作员)?在他们的文档中,有这句话 "Only meaningful when documents are in a defined order.",这是另一个证据。
  • 虽然答案是正确的,但建议的管道仍然非常低效,并且当有大量文档要推送到 $group 中的 items 数组时会失败。 (请参阅我的答案以了解如何避免这种情况 - 并更快地获得结果)
【解决方案2】:

$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

【讨论】:

  • $group doesn't guarantee the document order but it would keep the grouped documents in the sorted order for each bucket - 如果这是真的,那么我相信我的整个问题都是不必要的——这很好。您是否有某种文档来表明存储桶将保持排序?我在上面链接的原始 Mongo 线程似乎很清楚地表明 $group doesn't group by the order of input documents 这是重点,这里:(
  • 另外,我很欣赏显示如何使用 Reduce 手动对数组进行排序的巨大代码示例,但我很快就放弃了这种可能性:这显然是一个 O(N^2) 操作,对,因为你有嵌套reduce的?我不反对在项目阶段手动排序,我只是希望有更好的性能。
  • 我认为我自己和其他人实际上将这个问题理解为“如何保证$push'ed 项目的顺序”。因此,我认为这个问题错误地假定对 "$group 不维持秩序" 的评论实际上对分组 within 的数组有这种影响。因此,虽然“内联排序”会 nice (我多年来一直在抱怨这个)但实际上没有必要,因为$group 不会以问题询问/假设的方式影响数组。
  • 只是添加我对其他人注意到的评论,有几件事。一个是数组的排序选项,它将在不久的将来支持(更多在链接的 jira 问题中),因为展开解决方案不适用于大型数组。您链接的 jira 问题是关于维护输出文档的顺序而不是累加器,例如($first、$last 和 $push 设置的 $addToSet 除外)都采用先前排序的排序顺序,否则这些累加器不会任何意义。
  • 嘿只是想感谢您提供的所有重要信息,在这里,+1。另一个答案归零于我真正想要的东西,所以我给了那个人赏金。我希望没有难过的感觉!希望其他人会出现并给你更多的支持。
【解决方案3】:

由于$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 计划使用该索引,并且对于每个阶段,@ 987654329@ 将使用相同的索引按fk 的单个值进行过滤,并返回已经排序并限制为前两个的结果。至少在服务器实现https://jira.mongodb.org/browse/SERVER-9377 之前,这将是最有效的方法。

【讨论】:

  • 我猜您甚至不需要 $lookup 管道中的 $project,因为使用 $push of "$$ROOT" 您可以获得整个文档,所以如果不需要,请不要使用它。
猜你喜欢
  • 1970-01-01
  • 2019-01-02
  • 2013-03-01
  • 2011-09-25
  • 2018-12-17
  • 2021-04-13
  • 2020-02-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多