mongodb 数组求和问题

mongodb 中求和有两种方法可以进行数组求和,一种是分组求和,一种是 reduce 求和,但是前者在与 $unwind 联合使用的时候,会出现一些意想不到的问题,推荐使用第二种方式

假设

假设分别有如下集合:

  • student,学生表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [
    {
    name: 'user1',
    age: 18,
    },
    {
    name: 'user2',
    age: 19,
    },
    {
    name: 'user3',
    age: 18,
    }
    ]
  • achievement,成绩表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [
    {
    name: 'user1',
    courseName:'语文',
    score: '99'
    },
    {
    name: 'user1',
    courseName:'数学',
    score: '98'
    },
    {
    name: 'user2',
    courseName:'语文',
    score: '100'
    },
    {
    name: 'user2',
    courseName:'数学',
    score: '100'
    },
    ]

需求

  • 求学生的平均年龄
  • 求每个学生的总分数

解决方法

  1. 针对第一种需求,直接一个聚合就搞定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    db.aggregate([
    {
    $group:{
    _id:null,
    studentsCount: { $sum: 1 }
    studentsAge: { $sum: '$age' }
    }
    }
    ])

    // 结果
    [
    {
    studentsCount: 3,
    studentsAge: 55
    }
    ]

  2. 对第二种需求,就要分步讨论了。

    • achievement 集合中,每个学生都有分数录入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    db.aggregate([
    {
    $lookup:{
    from: 'achievement',
    localField: 'name',
    foreignField: 'name',
    as: 'achievements'
    }
    },
    {
    $unwind:'$achievements'
    },
    {
    $group:{
    _id:null,
    name: '$name',
    totalScore: { $sum: '$achievements.score' }
    }
    }
    ])

    • 像上面记录那样,user3 的分数还没有录入

    user3 还没有分数记录的时候,如果上面的聚合查询,由于使用了 $unwind,这就会导致一个问题,输出的成果里面,我们会发现,user3 丢失了。

    这是由于 $unwind 的特性导致的。官方的解释如下:

    1
    2
    3
    Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.

    解构数组的每个元素。输入文档的数组字段的值被数组元素替换后生成输出文档。

    所以,从上面可以知道,因为 $lookup 后,user3achievements 为空,导致解析时,就会丢失 user3,而我们想要的是,每一个 user 都有一个 totalScore,如果数组为空,就显示 0。

    所以,上述的分组求和不能满足我们的需求,我们可以采用 $project 来解决这个问题,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    db.aggregate([
    {
    $lookup:{
    from: 'achievement',
    localField: 'name',
    foreignField: 'name',
    as: 'achievements'
    }
    },
    {
    $project:{
    name: 1,
    totalScore:{
    // 使用方法见:https://docs.mongodb.com/manual/reference/operator/aggregation/reduce/
    $reduce:{
    input: '$achievements',
    initialValue: 0,
    in: {
    $add: ["$$value", "$$this.score"],
    },
    }
    }
    }
    }
    ])