一、group与sum的概念
1.知识储备:聚合与管道
1.1 MongoDB 聚合:
MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。
有点类似 SQL 语句中的 count(*)。
介绍一下聚合的中的一些表达式方法:
表达式 | 描述 | 实例 |
---|---|---|
$sum | 计算总和。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}]) |
$avg | 计算平均值 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}]) |
$min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}]) |
$max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}]) |
$push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}]) |
$addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}]) |
$first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}]) |
$last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}]) |
1.2 MongoDB 管道:
管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。
MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。
这里我们介绍一下聚合框架中常用的几个操作:
- $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
- $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
- $limit:用来限制MongoDB聚合管道返回的文档数。
- $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
- $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
- $group:将集合中的文档分组,可用于统计结果。
- $sort:将输入文档排序后输出。
- $geoNear:输出接近某一地理位置的有序文档。
1.3 聚合和管道的合作:
例子:这段将会把文章的分数在70到90之间的所有文章,然后将符合条件的记录送到下一阶段$group管道操作符进行处理。(所以理论上你可以定义一个聚合,在聚合下使用无数个管道)
group将会使用id进行分组,然后sum来计算每一个相同_id的总数并赋值给count。最后打印count的值就是所有符合条件的文章的数目了。
db.articles.aggregate( [ { $match : { score : { $gt : 70, $lte : 90 } } }, { $group: { _id: null, count: { $sum: 1 } } } ] );
此时看到此处,有的人可能会有疑问:
1.group使用id进行分组,那么id怎么为null;
2.sum进行计算,为什么给sum后面赋值个1;
别急,往下看:
2.正文:
group上面已解释是一种管道,将符合条件的进行分组,sum是聚合的计算方法。
sum可以在下面的管道或者方法中使用:
sum后面通常跟得是一个field(翻译为领域,我把它理解为变量领域,可能不太恰当,但是我找不到其它词去形容它):
1.如果这个变量领域是数值类型的,那么它会根据变量的数值进行计算;
2.如果这个变量领域有数值也有非数值类型的,它会根据那个数值类型的进行计算;
3.如果这个变量领域(不管是不是数值类型),只要他在表中没出现,那么它会返回0;
4.如果这个变量领域是非数值类型的,那么它会返回0;
看了sum后面的field的介绍,有的人可能马上反应过来,但是,有的人还可能一脸闷逼,那么我继续解释:
1.由于在group中使用_id进行分组,那么mogodb的group管道就会根据_id的具体值进行分组,但是当我们传入_id为:null时,mongodb的group此时就不会根据_id的具体值进行分组,它会把所有符合条件的记录看成一个整体,此时group取出的将是整个符合条件的所有记录。
2.由于sum后面跟的是1的值,那么sum在遍历每一个group组合的成员时,或者说sum在遍历变量领域的每一个成员的时候,由于这个变量领域里的值都是1,它每遍历到一个变量(表里的每条符合条件的记录)都会进行加1操作,此时由于group本身只有一个,就是一整个表中符合条件的所有记录,那么sum遍历整个表的每一条符合条件的记录时就会进行加1操作,随后count保存的就是所有符合条件的值。
所以通常情况下,我可以使用group+sum的组合写法,将数据库每条记录进行分类:
我有这样一个数据库:其中除了flag是布尔类型,agree是number类型,其它的都是字符串类型
现在我想做的是,如何计算每一条记录中的agree使他们相加起来,再返回给我:
例子:下面这段代码的执行过程:
1.使用group将每一个_id进行分组。
2.计算每一个agree的总和,由于我的_id是不重复的,所以sum的作用其实没有体现出来,不过当你的表里有多个记录是同一个id值,那么sum将会把每一个相同_id下的agree值进行计算,然后打印。
3.把计算后的每一个_id的agree总和作为commentAgreesCount的值。
4.打印输出。
db.article_comments.aggregate( [{ $group : { _id:"$_id", commentAgreesCount: { $sum: "$agree" }} }] )
运行结果: { "_id" : ObjectId("606a70ac298cbe2978047810"), "commentAgreesCount" : 1 } { "_id" : ObjectId("606a60ba3878e73b78a9f5f0"), "commentAgreesCount" : 1 } { "_id" : ObjectId("606a7694298cbe2978047814"), "commentAgreesCount" : 0 } { "_id" : ObjectId("606a5ce13878e73b78a9f5ec"), "commentAgreesCount" : 1 }
但是我的本意并不是想获得每一个分组后的结果,我是要获取所有的记录的agree总和。
所以解决方法:
1.把id定义为null,mongodb内部就会不区分_id,即把所有的_id进行计算,然后输出结果
db.article_comments.aggregate( [{ $group : { _id:null, commentAgreesCount: { $sum: "$agree" }} }] )
运行结果: { "_id" : null, "commentAgreesCount" : 3 }
但是sum方法只能计算数值类型的,如果是字符串类型的,它就会默认把值设置为0。这个我上面也有贴官网的图,也有进行相应的解释。
例子:
db.article_comments.aggregate( [{$group : { _id:null, commentAgreesCount: { $sum: "$topic_id" } } }] )
结果:
{ "_id" : null, "commentAgreesCount" : 0 }
3.后话:
如果你要查询的是非数值类型的总和,打比方:我的agree设置成了String类型,但是我想获得每一条记录中agree加起来的总和。
又或者是我有一个数据库,里面保存着这个月卖出去的所有商品,所有商品的id是字符类型的,我想统计所有商品的卖出情况,比如id:1的所有商品,因为id:1的商品一个月内可能卖出很多次。id:2的所有商品,因为id:2的商品一个月内也可能卖出很多次 ……以此类推,然后我想计算所有id相同的商品卖出去的情况,并使用sum进行计算所有相同id商品的价格*件数=每件商品的营业额
此时再假设你的商品价格也是字符串类型的,那么此时就无法用上面那个sum方法计算了,此时解决方法也有,就是根据数据库查询到的所有记录,再进行foreach循环遍历一下所有商品的价格字符,再把他们转换为数值类型的进行计算。
所以数据库的方法并不是万能的,有时候你无法根据数据库进行查询,获取想要的数据类型时,那么只能将数据库获得的冗余数据,使用其他语言再进行数据处理。