使用动态密钥累积文档

时间:2018-05-23 13:22:08

标签: mongodb mongodb-query aggregation-framework

我有一组看起来像这样的文件

{
   _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
}

我想我可以映射/减少这个,或者我可以分别查询平均值和计数。水果可能拥有的唯一价值是Apple和Orange。你还有什么方法可以做到这一点?我已经离开MongoDB一段时间了,也许还有新的惊人方法可以做到这一点我不知道?

2 个答案:

答案 0 :(得分:1)

聚合框架

聚合框架对你来说比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
}

我们用" 2" $group阶段,一次是积累"每个水果"然后使用"k""v"值下的$push将所有水果压缩成一个数组,以保持其#34;键#34;和他们的"计数"。我们对" key"做了一点改造。这里使用$toLower$concat来加入字符串。这在现阶段是可选的,但一般来说更容易。

"替代"因为我们已经积累了这些密钥,所以3.6只是在这个早期阶段而不是$mergeObjects中应用$push。它只是真正将$arrayToObject移动到管道中的不同阶段。它并不是真正必要的,并没有任何特定的优势。如果有任何东西它只是删除了灵活的选项,如"客户端转换"稍后讨论。

"平均"累积是通过$avg完成的,"bald"使用$cond进行计数,以测试字符串并将数字提供给$sum。随着阵列被卷起来#34;我们可以再次完成所有这些累积,以便完成所有事情。

如前所述,唯一真正依赖于"新功能的部分"都在$replaceRoot阶段,重写" root"文献。这就是为什么这是可选的,因为你可以在相同的"已经聚合的"之后简单地进行这些转换。数据从数据库返回。

我们在这里真正做的就是将该数组与"k""v"条目一起使用,并将其转换为"对象"通过$arrayToObject使用命名密钥,并使用我们已在" root"生成的其他密钥对该结果应用$mergeObjects。这会将该数组转换为结果中返回的主文档的一部分。

使用mongo shell兼容代码中的JavaScript Array.reduce()Object.assign()方法应用完全相同的转换。这是一个非常简单的应用,Cursor.map()通常是大多数语言实现的一个特性,因此您可以在开始使用游标结果之前进行这些转换。

使用ES6兼容的JavaScript环境(不是shell),我们可以稍微缩短语法:

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

所以它确实是一条"一条线"功能,这就是为什么像这样的转换在客户端代码中通常比服务器更好的原因。

作为关于$cond用法的说明,请注意将其用于"硬编码"由于几个原因,评估并不是一个好主意。所以对#34; force"真的没有多大意义。那个评价。即使使用您提供的数据,"bald"也会更好地表示为Boolean值,而不是"字符串"。如果您将"Yes/No"更改为true/false,那么即使是"一个"有效用法变为:

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

这消除了对测试"的需求。字符串匹配的条件,因为它已经true/false。 MongoDB 4.0使用$toInt添加另一个增强功能来强制使用"强制" Boolean到整数:

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

完全取消了对$cond的需求,就像记录10一样,但这种变化可能会导致数据清晰度下降,因此它仍然可能合理有那种强迫"在那里,但在其他任何地方都不是最佳。

即使是"动态"形式使用"两个" $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引入了" cursor"然后,对于聚合框架的输出,我们已经坚定地倾向于使用更新的选项。

事实上,大多数"遗产"使用MapReduce的原因基本上被它的年轻兄弟所取代,因为聚合框架获得了新的操作,从而消除了对JavaScript执行环境的需求。如果说对JavaScript执行的支持通常会逐渐减少,那将是一个公平的评论,并且一旦从一开始就使用它的遗留选项逐渐从产品中删除。

答案 1 :(得分:1)

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'
                }
            }
        },

    ]



);