MongoDB - безопасно сортировать внутренний массив после группы - PullRequest
4 голосов
/ 23 сентября 2019

Я пытаюсь найти все записи, соответствующие определенному условию, в данном случае _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]} }}
])

, и это работает, НО, это не гарантировано.Согласно эта нить Монго $group не гарантирует порядок документов.

Это также будет означать, что все предложенные здесь и в других решениях, которые рекомендуют использовать $unwind, затем $sort, а затем $group, также не будут работать по той же причине.

Каков наилучший способ сделать это с Mongo (любая версия)?Я видел предположения, что это может быть достигнуто в фазе $project, но я не совсем уверен, как.

Ответы [ 2 ]

1 голос
/ 30 сентября 2019

Вы правы, говоря, что результат $group никогда не сортируется.

$ group не упорядочивает свои выходные документы.

Следовательно, делаем;

{$sort: {fk: 1}}

, затем группируемся с

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

будет напрасным усилием.


Но есть серебряная подкладка с сортировкой до $group стадии с name: -1.Поскольку вы используете $push (не $addToSet), вставленные объекты сохранят порядок, который они имели во вновь созданном массиве items в $groupрезультат.Вы можете увидеть это поведение здесь, пример вашего конвейера

Массив items будет всегда иметь;

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

в том же порядке, поэтому сортировка вложенных массивов не является проблемой 1044 *!Хотя я не могу найти точную цитату в документации, чтобы подтвердить это поведение, вы можете проверить;

  • this ,
  • или this где это подтверждается.
  • Кроме того, список операторов аккумулятора для $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 голос
/ 26 сентября 2019

Обновление: я нашел jira, который разрешит сортировку массива.

Вы можете попробовать ниже $project stage, чтобы отсортировать массив. Возможно, есть другой способсделать это.Это должно сортировать имена по убыванию. Рабочее, но более медленное решение.

{"$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]

Справочник - Сортировка массива с помощью JavaScript, уменьшить функцию

$ group не гарантирует порядок документов, но будет сохранять сгруппированные документы в отсортированном порядке для каждого сегмента.Так что в вашем случае, даже если документы после этапа $ group не отсортированы по fk, а каждая группа (элементы) будет отсортирована по убыванию имени.Если вы хотите сохранить документы, отсортированные по fk, вы можете просто добавить {$sort:{fk:1}} после $ group stage

Вы также можете отсортировать по порядку значений, переданных в запросе на совпадение, если вам нужно, добавив дополнительное поле.за каждый документ.Что-то вроде

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: {ifk : 1}},
    {$project: {items: {$slice: ["$items", 2]}}}
])
...