如何筛选数组值和获得平均值

时间:2017-09-01 10:49:23

标签: mongodb aggregation-framework

我正在使用着名的餐厅系列在mongoDB上练习。我有一个像这样的记录列表:

{
"_id" : ObjectId("59a5211e107765480896f3e5"),
"address" : {
    "building" : "1007",
    "coord" : [
        -73.856077,
        40.848447
    ],
    "street" : "Morris Park Ave",
    "zipcode" : "10462"
},
"borough" : "Bronx",
"cuisine" : "Bakery",
"grades" : [
    {
        "date" : ISODate("2014-03-03T00:00:00Z"),
        "grade" : "A",
        "score" : 2
    },
    {
        "date" : ISODate("2013-09-11T00:00:00Z"),
        "grade" : "A",
        "score" : 6
    },
    {
        "date" : ISODate("2013-01-24T00:00:00Z"),
        "grade" : "A",
        "score" : 10
    },
    {
        "date" : ISODate("2011-11-23T00:00:00Z"),
        "grade" : "A",
        "score" : 9
    },
    {
        "date" : ISODate("2011-03-10T00:00:00Z"),
        "grade" : "B",
        "score" : 14
    }
],
"name" : "Morris Park Bake Shop",
"restaurant_id" : "30075445"
}

我想计算grade.score的平均值,仅使用grade.grade ='A'的元素。我正在做的是

db.restaurants.aggregate([{$unwind: '$grades'}, {$filter: {input: '$grades.grade', as: 'grade', cond: {'$$grade': 'A'}}}, {$group: {_id: '$name', 'avg': {'$avg': '$grades.score'}}}, {$sort: {'avg': 1}}])

出了什么问题?

感谢

1 个答案:

答案 0 :(得分:0)

$filter运算符不是单独的管道阶段。它只返回一个数组作为属性。在这种情况下,它最好直接在$group中用作$avg的参数:

db.restaurants.aggregate([
  { '$group': {
    '_id': '$name',
    'avg': {
      '$avg': {
        '$avg': {
          '$map': {
            'input': {
              '$filter': {
                'input': '$grades', 
                'as': 'grade',
                'cond': { '$eq': [ '$$grade.grade',  'A' ] }
              }
            },
            'as': 'grade',
            'in': '$$grade.score'
          }
        }
      }
    }
  }}, 
  { '$sort': { 'avg': 1 } }
])

同时应用$map仅提取"score"值并将其提供给$avg,其显示为"两次",一次创建"平均"文档中的值,其次是"平均累加器"对于分组键。

对于问题中显示的数据,您会得到:

{
    "_id" : "Morris Park Bake Shop",
    "avg" : 6.75
}

只有那些标有等级" A"

的条目的平均分数

有趣的是,这在单个文档中工作正常,但如果应用于https://raw.githubusercontent.com/mongodb/docs-assets/primer-dataset/primer-dataset.json的完整数据集,则会产生null值,以获取实际上未累积的任何$avg超过一个文件。

只需将过滤后的数组中的平均值添加到文档即可:

db.restaurants.aggregate([
  { "$addFields": {
    "average": {
      "$avg": {
         "$map": { 
          "input": {
            "$filter": {
              "input": "$grades",
              "as": "g",
              "cond": { "$eq": [ "$$g.grade", "A" ]  }    
            }
          },
          "as": "g",
          "in": "$$g.score"
        }
      }
    }
  }}
])

累积超过一个文档。即"cuisine"

db.restaurants.aggregate([
  { '$group': {
    '_id': '$cuisine',
    'avg': {
      '$avg': {
        '$avg': {
          '$map': {
            'input': {
              '$filter': {
                'input': '$grades', 
                'as': 'g',
                'cond': { '$eq': [ '$$g.grade',  'A' ] }
              }
            },
            'as': 'g',
            'in': '$$g.score'
          }
        }
      }
    }
  }}, 
  { '$sort': { 'avg': 1 } }
])

这意味着按照最初的说法工作,并且当值为"分组为"实际上发生在多个文档中。

遗憾的是,无论分组了多少文档,唯一可以应用的可靠方法仍然是$unwind。现代版本中真的不需要这样做:

虽然这会产生一致的结果:

db.restaurants.aggregate([
  { "$match": { "grades.grade": "A" } },
  { "$unwind": "$grades" },
  { "$match": { "grades.grade": "A" } },
  { "$group": {
     "_id": "$name",
     "score": { "$avg": "$grades.score" } 
  }},
  { "$sort": { "score": -1 } }
])

最佳选择,实际比较"苹果与苹果"是预先过滤任何"文件"仅适用于那些在初始阶段可能具有与$match条件相匹配的数组元素的那些:

db.restaurants.aggregate([
  { '$match': { 'grades.grade': 'A' } },
  { '$group': {
    '_id': '$name',
    'value': {
      '$avg': {
        '$avg': {
          '$map': {
            'input': {
              '$filter': {
                'input': '$grades', 
                'as': 'g',
                'cond': { '$eq': [ '$$g.grade',  'A' ] }
              }
            },
            'as': 'g',
            'in': '$$g.score'
          }
        }
      }
    }
  }},
  { "$sort": { "value": -1 } }
])

此外,$sort通常会应用于"否定"或"降序"顺序,首先表示最大值。或者至少在你检查数据和掌握聚合时最有意义。

看起来更短或可能更少混淆"并不意味着"更好" 。我们在$filter管道中使用$map$group操作编写此内容的原因是因为使用$unwind进行处理的成本非常高。

使用$unwind为每个阵列成员创建整个文档的副本,这通常相当于要处理的文档数量的大量增加,这当然会增加处理时间。

所以"更短的真实案例更好"实际上是在使用"较少的管道阶段"并且通过完全删除$unwind的所有用法来不夸大要处理的文档数量。

$match之前添加$group的原因是因为在另一个示例中,当您使用$unwind进行处理时,任何空数组都将从要处理的文档中删除,并且然后其他后续的$match将过滤掉没有"A"成绩的任何内容以及确实有成绩但没有匹配"A"的文档也会被删除。

因此,使用$map$filter,这些文档将返回null,因为这是从$avg的空数组参数返回的值。但当然,如果最初的条件是"文件"必须包含匹配的条件,然后最初为空或过滤空的"数组永远不会被考虑,因为它们一开始就从处理中删除了。

作为任何聚合操作的黄金法则,包括某种程度的"过滤"是始终 $match作为第一个管道阶段,因此只有那些对任何后续条件有效的文档才是唯一选择的文档。这也大大加快了它的速度。

  

注意:由于返回了null值,这导致我在疲惫状态下出现了一些相当大的恐慌。应该注意的是"通常"任何$group操作的应用通常是积累和减少"结果很大。

     

在问题中选择的"$name"字段对于从MongoDB文档示例中获取的sample dataset中的每个文档都非常独特。一个更现实的分组"样本将是从数据中使用"$cuisine",这实际上是在文档中累积的#34}。您通常使用

相关问题