Накопить документы с помощью динамических ключей - PullRequest
0 голосов
/ 23 мая 2018

У меня есть коллекция документов, которая выглядит следующим образом

{
   _id: 1,
   weight: 2,
   height: 3,
   fruit: "Orange",
   bald: "Yes"
},
{
   _id: 2,
   weight: 4,
   height: 5,
   fruit: "Apple",
   bald: "No"
}

Мне нужно получить результат, который объединяет всю коллекцию в это.

{
   avgWeight: 3,
   avgHeight: 4,
   orangeCount: 1,
   appleCount: 1,
   baldCount: 1
}

Я думаю, что смогу отобразить/ уменьшить это, или я мог бы запросить средние значения и числа отдельно.Единственные ценности, которые фрукт может когда-либо иметь - это яблоко и апельсинКакие еще способы вы бы сделали для этого?Я уже давно ухожу из MongoDB, и, может быть, есть новые удивительные способы сделать это, о которых я не знаю?

Ответы [ 2 ]

0 голосов
/ 23 мая 2018
db.demo.aggregate(

    // Pipeline
    [
        // Stage 1
        {
            $project: {
                weight: 1,
                height: 1,
                Orange: {
                    $cond: {
                        if: {
                            $eq: ["$fruit", 'Orange']
                        },
                        then: {
                            $sum: 1
                        },
                        else: 0
                    }
                },
                Apple: {
                    $cond: {
                        if: {
                            $eq: ["$fruit", 'Apple']
                        },
                        then: {
                            $sum: 1
                        },
                        else: 0
                    }
                },
                bald: {
                    $cond: {
                        if: {
                            $eq: ["$bald", 'Yes']
                        },
                        then: {
                            $sum: 1
                        },
                        else: 0
                    }
                },
            }
        },

        // Stage 2
        {
            $group: {
                _id: null,
                avgWeight: {
                    $avg: '$weight'
                },
                avgHeight: {
                    $avg: '$height'
                },
                orangeCount: {
                    $sum: '$Orange'
                },
                appleCount: {
                    $sum: '$Apple'
                },
                baldCount: {
                    $sum: '$bald'
                }
            }
        },

    ]



);
0 голосов
/ 23 мая 2018

Структура агрегации

Структура агрегации будет работать намного лучше, чем то, что может сделать mapReduce, и базовый метод совместим с каждым выпуском обратно до 2.2, когда была выпущена структура агрегации.

Если у вас есть MongoDB 3.6 , вы можете сделать

db.fruit.aggregate([
  { "$group": {
    "_id": "$fruit",
    "avgWeight": { "$avg": "$weight" },
    "avgHeight": { "$avg": "$height" },
    "baldCount": {
      "$sum": { "$cond": [{ "$eq": ["$bald", "Yes"] }, 1, 0] }
    },
    "count": { "$sum": 1 }
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$push": { 
         "k": { 
           "$concat": [
             { "$toLower": "$_id" },
             "Count"
           ]
         }, 
         "v": "$count"
      }
    },
    "avgWeight": { "$avg": "$avgWeight" },
    "avgHeight": { "$avg": "$avgHeight" },
    "baldCount": { "$sum": "$baldCount" }
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        { "$arrayToObject": "$data" },
        {
          "avgWeight": "$avgWeight",
          "avgHeight": "$avgHeight",
          "baldCount": "$baldCount"
        }      
      ]
    }  
  }}
])

В качестве небольшой альтернативы, вы можете применить $mergeObjects в $group здесь вместо этого:

db.fruit.aggregate([
  { "$group": {
    "_id": "$fruit",
    "avgWeight": { "$avg": "$weight" },
    "avgHeight": { "$avg": "$height" },
    "baldCount": {
      "$sum": { "$cond": [{ "$eq": ["$bald", "Yes"] }, 1, 0] }
    },
    "count": { "$sum": 1 }
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$mergeObjects": {
        "$arrayToObject": [[{
          "k": { 
            "$concat": [
              { "$toLower": "$_id" },
              "Count"
            ]
          }, 
          "v": "$count"
        }]]
      }
    },
    "avgWeight": { "$avg": "$avgWeight" },
    "avgHeight": { "$avg": "$avgHeight" },
    "baldCount": { "$sum": "$baldCount" }
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        "$data",
        {
          "avgWeight": "$avgWeight",
          "avgHeight": "$avgHeight",
          "baldCount": "$baldCount"
        }      
      ]
    }
  }}
])

Но есть причины, почему я лично не считаю, что это лучший подход, и это в основном приводит к следующей концепции.

Так что даже еслиу вас нет «последней» версии MongoDB, вы можете просто изменить форму вывода, так как это последний этап конвейера, фактически использующий возможности MongoDB 3.6:

db.fruit.aggregate([
  { "$group": {
    "_id": "$fruit",
    "avgWeight": { "$avg": "$weight" },
    "avgHeight": { "$avg": "$height" },
    "baldCount": {
      "$sum": { "$cond": [{ "$eq": ["$bald", "Yes"] }, 1, 0] }
    },
    "count": { "$sum": 1 }
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$push": { 
         "k": { 
           "$concat": [
             { "$toLower": "$_id" },
             "Count"
           ]
         }, 
         "v": "$count"
      }
    },
    "avgWeight": { "$avg": "$avgWeight" },
    "avgHeight": { "$avg": "$avgHeight" },
    "baldCount": { "$sum": "$baldCount" }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        { "$arrayToObject": "$data" },
        {
          "avgWeight": "$avgWeight",
          "avgHeight": "$avgHeight",
          "baldCount": "$baldCount"
        }      
      ]
    }  
  }}
  */
]).map( d =>
  Object.assign(
    d.data.reduce((acc,curr) => Object.assign(acc,{ [curr.k]: curr.v }), {}),
    { avgWeight: d.avgWeight, avgHeight: d.avgHeight, baldCount: d.baldCount }
  )
)

И, конечно, вы можете дажепросто «жестко закодируйте» ключи:

db.fruit.aggregate([
  { "$group": {
    "_id": null,
    "appleCount": {
      "$sum": {
        "$cond": [{ "$eq": ["$fruit", "Apple"] }, 1, 0]
      }
    },
    "orangeCount": {
      "$sum": {
        "$cond": [{ "$eq": ["$fruit", "Orange"] }, 1, 0]
      }
    },
    "avgWeight": { "$avg": "$weight" },
    "avgHeight": { "$avg": "$height" },
    "baldCount": {
      "$sum": {
        "$cond": [{ "$eq": ["$bald", "Yes"] }, 1, 0]
      }
    }
  }}
])

Но это не рекомендуется, поскольку ваши данные могут измениться когда-нибудь, и если есть значение для «группировки», то лучше использовать его на самом деле, чемпринуждение сусловия.

В любой форме вы возвращаете один и тот же результат:

{
        "appleCount" : 1,
        "orangeCount" : 1,
        "avgWeight" : 3,
        "avgHeight" : 4,
        "baldCount" : 1
}

Мы делаем это с "двумя" $group ступенями, то есть один раз для накопления "зафрукты ", а затем, во-вторых, сжать все фрукты в массив, используя $push в значениях "k" и "v", чтобы сохранить их" ключ "и их" количество ".Мы делаем небольшое преобразование «ключа», используя $toLower и $concat, чтобы соединить строки.Это необязательно на данном этапе, но проще в целом.

«Альтернатива» для 3.6 просто применяет $mergeObjects на этом более раннем этапе вместо $pushтак как мы уже накопили эти ключи.Это просто действительно перемещает $arrayToObject на другую стадию в конвейере.Это на самом деле не нужно и не имеет особых преимуществ.Во всяком случае, он просто удаляет гибкую опцию, как продемонстрировано в «клиентском преобразовании», которое обсуждается ниже.

«Среднее» накопление производится через $avg, а "bald" подсчитывается с использованием$cond для проверки строк и ввода числа в $sum.Поскольку массив «свернут», мы можем снова сделать все эти накопления, чтобы получить все для всех.

Как уже упоминалось, единственная часть, которая фактически опирается на «новые функции», находится внутри $replaceRoot этап, который переписывает «корневой» документ.Вот почему это необязательно, так как вы можете просто выполнить эти преобразования после того, как те же «уже агрегированные» данные будут возвращены из базы данных.

Все, что мы на самом деле здесь делаем, это берем этот массив с "k" и "v"записи и превратить его в «объект» с именованными ключами через $arrayToObject и применить $mergeObjects к этому результату с другими ключами, которые мы уже произвели в «корне».Это преобразует этот массив в часть основного документа, возвращаемого в результате.

Точно такое же преобразование применяется с использованием методов JavaScript Array.reduce() и Object.assign()в коде, совместимом с mongo.Это очень простая вещь для применения, и Cursor.map(), как правило, является функцией большинства языковых реализаций, поэтому вы можете выполнить эти преобразования до того, как начнете использовать результаты курсора.

С ES6-совместимыми средами JavaScript (не оболочкой)), мы можем сократить этот синтаксис еще немного:

.map(({ data, ...d }) => ({ ...data.reduce((o,[k,v]) => ({ ...o, [k]: v }), {}), ...d }) )

Таким образом, это действительно «однострочная» функция, и это общая причина, почему подобные преобразования часто лучше в клиентском коде, чем на сервере.в любом случае.

В качестве примечания по использованию $cond отмечается, что использование его для «жестко закодированной» оценки не очень хорошая идея по нескольким причинам.Так что на самом деле не имеет смысла «форсировать» эту оценку.Даже с данными, которые вы предоставляете, "bald" будет лучше выражаться как значение Boolean, чем "строка".Если вы измените "Yes/No" на true/false, тогда даже это «одно» действительное использование станет:

"baldCount": { "$sum": { "$cond": ["$bald", 1, 0 ] } }

Это устраняет необходимость «проверять» условие на совпадение строк, поскольку оно уже true/false.MongoDB 4.0 добавляет еще одно улучшение, используя $toInt, чтобы "принудительно" привести Boolean к целому числу:

"baldCount": { "$sum": { "$toInt": "$bald" } }

, что устраняет необходимость в $condв целом, как при простой записи 1 или 0, но это изменение может привести к потере ясности в данных, поэтому все же, вероятно, разумно иметь такого рода «принуждение» там, но не совсем оптимально где-либо еще.

Даже с «динамической» формой, использующей «две» $group ступеней для накопления, основная работа все еще выполняется на первом этапе.Оставшееся накопление просто сохраняется в n результирующих документах для числа возможных уникальных значений ключа группировки.В данном случае «два», так что, хотя это и дополнительная инструкция, для получения гибкого кода не требуется никаких дополнительных затрат.


MapReduce

Если у вас действительно естьсердце, по крайней мере, «пробует» mapReduce, тогда это действительно один проход с функцией finalize только для того, чтобы получить средние значения

db.fruit.mapReduce(
  function() {
    emit(null,{ 
      "key": { [`${this.fruit.toLowerCase()}Count`]: 1 },
      "totalWeight": this.weight,
      "totalHeight": this.height,
      "totalCount": 1,
      "baldCount": (this.bald === "Yes") ? 1 : 0
    });
  },
  function(key,values) {
    var output = {
      key: { },
      totalWeight: 0,
      totalHeight: 0,
      totalCount: 0,
      baldCount: 0
    };

    for ( let value of values ) {
      for ( let key in value.key ) {
        if ( !output.key.hasOwnProperty(key) )
          output.key[key] = 0;

        output.key[key] += value.key[key];
      }

      Object.keys(value).filter(k => k != 'key').forEach(k =>
        output[k] += value[k]
      )
    }

    return output;
  },
  { 
    "out": { "inline": 1 },
    "finalize": function(key,value) {
      return Object.assign(
        value.key,
        {
          avgWeight: value.totalWeight / value.totalCount,
          avgHeight: value.totalHeight / value.totalCount,
          baldCount: value.baldCount
        }
      )
    }
  }
)

, поскольку мы уже прошли процесс для aggregate() метод, тогда общие пункты должны быть довольно знакомы, так как мы в основном делаем здесь то же самое.

Основные различия заключаются в том, что для «среднего» вам действительно нужны полные итоги и числа, и, конечно, вы получаетенемного больше контроля над накоплением через «Объект» с кодом JavaScript.

Результаты в основном те же, только со стандартным mapReduce «изогнутым» способом их представления:

  {
      "_id" : null,
      "value" : {
        "orangeCount" : 1,
        "appleCount" : 1,
        "avgWeight" : 3,
        "avgHeight" : 4,
        "baldCount" : 1
      }
  }

Сводка

Основная сложность заключается в том, что MapReduce, использующий интерпретированный JavaScript для выполнения, имеет гораздо более высокую стоимость и более медленное выполнение, чемродные кодированные операции структуры агрегации. Возможно, когда-то была возможность использовать MapReduce для этого вида вывода на «больших» наборах результатов, но поскольку MongoDB 2.6 ввел вывод «курсора» для структуры агрегации, тогда весы были твердо измененыв пользу более новой опции.

Факт заключается в том, что большинство «унаследованных» причин для использования MapReduce в основном вытесняется его младшим братом, поскольку структура агрегации получает новые операции, которые устраняют необходимость в среде выполнения JavaScript.Было бы справедливо сказать, что поддержка выполнения JavaScript обычно "истощается", и как только устаревшие варианты, которые использовали это с самого начала, постепенно удаляются из продукта.

...