使用mongodb聚合从包含对象的数组中获取平均值

时间:2018-07-28 15:18:19

标签: mongodb aggregation-framework

在使用mongodb聚合管道方法在测试中回答问题的所有学生获得分​​数时,我遇到了问题

我的聚合管道提供了一系列对象,该对象由每个学生对测试问题的答案组成。

管道将类似于下面的管道,我的示例从我的实际问题中简化了。基本上,我将每个用户的每个问题数组分组并将其推入“分数”字段。然后,我使用reduce来简化分数字段

{ $group: { 
    _id: {}, 
    scores: { $push: "$questions" }
} }, 
{ $addFields: { 
    testScores: {
        $reduce: {
            input: "$scores",
            initialValue: [ ],
            in: { $concatArrays: [ "$$value", "$$this" ] }
        }
    } 
} }

结果类似于:

testScores: [
    { id: 'questionOne' score: 1 }, 
    { id: 'questionOne', score: 3 }, 
    { id: 'questionOne', score: 8 }, 
    { id: 'questionOne' score: 2 },
    ....
    { id: 'questionFifty' score: 1 }, 
    { id: 'questionFifty', score: 3 }, 
    { id: 'questionFifty', score: 8 }, 
    { id: 'questionFifty' score: 2 }
]

我的问题是,如何获得“ questionOne”和所有其他问题的所有分数的平均分数? 我无法解散数组,因为我进行了大量测试,因此mongoDb似乎无法解散足够数量的数据,而不会为聚合结果返回null。

在JavaScript中,我将使用reduce,但据我所知,mongodb允许在reduce函数之外使用vars,但据我所知,您无法修改reduce函数,因此不可能实现与下面的函数类似的内容。 / p>

myArray.reduce((acc, next){
    if(acc[next.id]}{
       acc[next.id].score += next.score
       acc[next.id].count+= 1
       acc[next].avg = acc[next.id].score/acc[next.id].count
    }else{
        acc[next.id].score = next.score
        acc[next.id].count = 1
    }
 return acc
},{} }

感谢任何指针

1 个答案:

答案 0 :(得分:0)

是的,$reduce可以实现,但有两个重要警告:

    $let内的
  • 内部vars中,您只能引用在外部作用域中定义的变量,但不能在同一块中定义多个变量并互相引用-这就是为什么在该解决方案中有很多嵌套
  • $reduce公开了$$value变量,该变量表示聚合的当前状态。事实是,您应该将该变量视为 immutable ,这意味着您可以引用它,但不能对其进行修改

然后您可以尝试以下聚合:

db.col.aggregate([
    {
        $project: {
            averages: {
                $reduce: {
                    input: "$testScores",
                    initialValue: [],
                    in: {
                        $let: {
                            vars: {
                                index: { $indexOfArray: [ "$$value.id", "$$this.id" ] }
                            },
                            in: {
                                $let: {
                                    vars: { 
                                        prev: { 
                                            $cond: [ { $ne: [ "$$index", -1 ] }, { $arrayElemAt: [ "$$value", "$$index" ] }, { id: "$$this.id", score: 0, count: 0 } ] 
                                        } 
                                    },
                                    in: {
                                        $let: {
                                            vars: {
                                                updated: {
                                                    id: "$$prev.id",
                                                    score: { $add: [ "$$prev.score", "$$this.score" ] },
                                                    count: { $add: [ "$$prev.count", 1 ] },
                                                    avg: {
                                                        $divide: [ { $add: [ "$$prev.score", "$$this.score" ] }, { $add: [ "$$prev.count", 1 ] } ]
                                                    }
                                                }
                                            },
                                            in: {
                                                $cond: {
                                                    if: { $eq: [ "$$index", -1 ] },
                                                    then: { $concatArrays: [ "$$value", [ "$$updated" ] ] },
                                                    else: { $concatArrays: [ { $slice: [ "$$value", "$$index"] }, [ "$$updated" ], { $slice: [ "$$value", { $add: [ "$$index", 1 ] }, { $size: "$$value" }] } ] }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
])

实际上每个$let都在此处定义了算法步骤:

  • 使用$arrayElemAt检查当前处理的文档是否汇总
  • 使用$cond$arrayElemAt获取当前处理的id的先前值或提供默认值
  • 然后在updated下计算新值,包括average

要返回该值,我们应该考虑两种情况:

  • 如果没有先前的值,则只需将updated文档附加到当前数组(使用$concatArrays
  • 当我们必须“更新”现有聚合时,我们必须删除旧值并添加新值-$slice可在此处用于获取当前$$index之前和之后的文档

对于您的示例,它输出:

{
    "averages" : [
            {
                    "id" : "questionOne",
                    "score" : 14,
                    "count" : 4,
                    "avg" : 3.5
            },
            {
                    "id" : "questionFifty",
                    "score" : 14,
                    "count" : 4,
                    "avg" : 3.5
            }
    ]
}