文章目录
MongoDB:mongodb的聚合和管道
一、聚合
简介
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果
聚合表达式
表达式 | 描述 | 实例 |
---|---|---|
$sum | 计算总和。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { s u m : " sum : " sum:"likes"}}}]) |
$avg | 计算平均值 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { a v g : " avg : " avg:"likes"}}}]) |
$min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { m i n : " min : " min:"likes"}}}]) |
$max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { m a x : " max : " max:"likes"}}}]) |
$push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", url : { p u s h : " push: " push:"url"}}}]) |
$addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", url : { a d d T o S e t : " addToSet : " addToSet:"url"}}}]) |
$first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", first_url : { f i r s t : " first : " first:"url"}}}]) |
$last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", last_url : { l a s t : " last : " last:"url"}}}]) |
案例
数据准备
> db.user.find()
{ "_id" : ObjectId("62216f5880900fc3a9cad273"), "title" : "mysql", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 100, "url" : "www.baidu.com" }
{ "_id" : ObjectId("62216f6b80900fc3a9cad274"), "title" : "redis", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 200, "url" : "www.redis.com" }
{ "_id" : ObjectId("62216f7b80900fc3a9cad275"), "title" : "mongodb", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 300, "url" : "www.mongodb.com" }
现在我们通过以上集合计算每个作者所写的文章数,使用aggregate()计算结
> db.user.aggregate([{$group:{_id:"$by_user",num_tutorial:{$sum:1}}}])
{ "_id" : "admin", "num_tutorial" : 3 }
计算最大值
> db.user.aggregate([{$group:{_id:"$by_user",num_tutorial:{$max:"$likes"}}}])
{ "_id" : "admin", "num_tutorial" : 300 }
分组计算最后一个和第一个
> db.user.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])
{ "_id" : "admin", "first_url" : "www.baidu.com" }
> db.user.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}])
{ "_id" : "admin", "last_url" : "www.mongodb.com" }
二、管道
简介
管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。
MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。
表达式
- $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
- m a t c h : 用 于 过 滤 数 据 , 只 输 出 符 合 条 件 的 文 档 。 match:用于过滤数据,只输出符合条件的文档。 match:用于过滤数据,只输出符合条件的文档。match使用MongoDB的标准查询操作。
- $limit:用来限制MongoDB聚合管道返回的文档数。
- $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档
- $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值
- $group:将集合中的文档分组,可用于统计结果
- $sort:将输入文档排序后输出
- $geoNear:输出接近某一地理位置的有序文档
案例
$match
> db.user.aggregate({$match:{"title":"mysql"}})
{ "_id" : ObjectId("62216f5880900fc3a9cad273"), "title" : "mysql", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 100, "url" : "www.baidu.com" }
$project
提取字段
> db.user.find()
{ "_id" : ObjectId("62216f5880900fc3a9cad273"), "title" : "mysql", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 100, "url" : "www.baidu.com" }
{ "_id" : ObjectId("62216f6b80900fc3a9cad274"), "title" : "redis", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 200, "url" : "www.redis.com" }
{ "_id" : ObjectId("62216f7b80900fc3a9cad275"), "title" : "mongodb", "by_user" : "admin", "tags" : [ "mongodb", "mysql", "redis" ], "likes" : 300, "url" : "www.mongodb.com" }
> db.user.aggregate({$project:{title:1,_id:0}})
{ "title" : "mysql" }
{ "title" : "redis" }
{ "title" : "mongodb" }
1表示要该字段,0表示不要该字段,也可以对返回的字段进行重命名,比如将title改为articleTitle,如下:
> db.user.aggregate({$project:{"articleTitle":"$title"}})
{ "_id" : ObjectId("62216f5880900fc3a9cad273"), "articleTitle" : "mysql" }
{ "_id" : ObjectId("62216f6b80900fc3a9cad274"), "articleTitle" : "redis" }
{ "_id" : ObjectId("62216f7b80900fc3a9cad275"), "articleTitle" : "mongodb" }
不过这里有一个问题需要注意,如果原字段上有索引,重命名之后的字段上就没有索引了,因此最好在重命名之前使用索引
数学表达式
数据准备
> db.user.save({"orderAddressL" : "ShenZhen","prodMoney" : 45.0,"freight" : 13.0,"discounts" : 3.0,"orderDate" : ISODate("2017-10-31T09:27:17.342Z"),"prods" : [ "可乐", "奶茶"]})
订单的总费用为商品费用加上运费,查询如下:
> db.user.aggregate({$project:{totalMoney:{$add:["$prodMoney","$freight"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "totalMoney" : 58 }
实际付款的费用是总费用减去折扣,如下:
> db.user.aggregate({$project:{totalPay:{$subtract:[{$add:["$prodMoney","$freight"]},"$discounts"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "totalPay" : 55 }
计算prodMoney和freight和discounts的乘积:
> db.user.aggregate({$project:{test1:{$multiply:["$prodMoney","$freight","$discounts"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test1" : 1755 }
再比如求freight的商,如下:
> db.user.aggregate({$project:{test1:{$divide:["$prodMoney","$freight"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test1" : 3.4615384615384617 }
再比如用prodMoney取模,如下:
> db.user.aggregate({$project:{test1:{$mod:["$prodMoney","$freight"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test1" : 6 }
加法和乘法都可以接收多个参数,其余的都接收两个参数。
日期表达式
日期表达式可以从一个日期类型中提取出年、月、日、星期、时、分、秒等信息,如下:
> db.user.aggregate({$project:{"年份":{$year:"$orderDate"},"月份":{$month:"$orderDate"},"一年中第几周":{$week:"$orderDate"},"日期":{$dayOfMonth:"$orderDate"},"星期":{$dayOfWeek:"$orderDate"},"一年中第几天":{$dayOfYear:"$orderDate"},"时":{$hour:"$orderDate"},"分":{$minute:"$orderDate"},"秒":{$second:"$orderDate"},"毫秒":{$millisecond:"$orderDate"},"自定义格式化时间":{$dateToString:{format:"%Y年%m月%d %H:%M:%S",date:"$orderDate"}}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "年份" : 2017, "月份" : 10, "一年中第几周" : 44, "日期" : 31, "星期" : 3, "一年中第几天" : 304, "时" : 9, "分" : 27, "秒" : 17, "毫秒" : 342, "自定义格式化时间" : "2017年10月31 09:27:17" }
week表示本周是本年的第几周,从0开始计。$dateToString
是MongoDB3.0+中的功能
字符串表达式
字符串表达式中有字符串的截取、拼接、转大写、转小写等操作,比如我截取orderAddressL前两个字符返回,如下:
> db.user.aggregate({$project:{addr:{$substr:["$orderAddressL",0,2]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "addr" : "Sh" }
再比如我将orderAddressL和orderDate拼接后返回:
> db.user.aggregate({$project:{addr:{$concat:["$orderAddressL",{$dateToString:{format:"--%Y年%m月%d",date:"$orderDate"}}]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "addr" : "ShenZhen--2017年10月31" }
再比如我将orderAddressL全部转为小写返回:
> db.user.aggregate({$project:{addr:{$toLower:"$orderAddressL"}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "addr" : "shenzhen" }
再比如我将orderAddressL全部转为大写返回:
> db.user.aggregate({$project:{addr:{$toUpper:"$orderAddressL"}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "addr" : "SHENZHEN" }
逻辑表达式
想要比较两个数字的大小,可以使用$cmp
操作符,如下:
> db.user.aggregate({$project:{test:{$cmp:["$freight","$discounts"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test" : 1 }
如果第一个参数大于第二个参数返回正数,第一个参数小于第二个则返回负数,
也可以利用$strcasecmp来比较字符串:
> db.user.aggregate({$project:{test:{$strcasecmp:[{$dateToString:{format:"..%Y年%m月%d",date:"$orderDate"}},"$orderAddressL"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test" : -1 }
至于我们之前介绍的ne/gte/lte
等操作符在这里一样是适用的。另外还有or、and为例,如下:
> db.user.aggregate({$project:{test:{$and:[{"$eq":["$freight","$prodMoney"]},{"$eq":["$freight","$discounts"]}]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test" : false }
or则表示参数中有一个为true就返回true,$not
则会对它的参数的值取反,如下:
> db.user.aggregate({$project:{test:{$not:{"$eq":["$freight","$prodMoney"]}}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test" : true }
另外还有两个流程控制语句,如下:
> db.user.aggregate({$project:{test:{$cond:[false,"trueExpr","falseExpr"]}}})
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "test" : "falseExpr" }
$cond
第一个参数如果为true,则返回trueExpr,否则返回falseExpr
db.user.aggregate({$project:{test:{$ifNull:[null,"replacementExpr"]}}})
$ifNull
第一个参数如果为null,则返回replacementExpr,否则就返回第一个参数。
$group
> db.user.find()
{ "_id" : ObjectId("6221836f80900fc3a9cad276"), "orderAddressL" : "ShenZhen", "prodMoney" : 45, "freight" : 13, "discounts" : 3, "orderDate" : ISODate("2017-10-31T09:27:17.342Z"), "prods" : [ "可乐", "奶茶" ] }
{ "_id" : ObjectId("62218d1180900fc3a9cad277"), "orderAddressL" : "ShenZhen", "prodMoney" : 45, "freight" : 20, "discounts" : 3, "orderDate" : ISODate("2017-10-31T09:27:17.342Z"), "prods" : [ "可乐", "奶茶" ] }
基本操作
$group
可以用来对文档进行分组,比如我想将订单按照城市进行分组,并统计出每个城市的订单数量:
> db.user.aggregate({$group:{_id:"$orderAddressL",count:{$sum:1}}})
{ "_id" : "ShenZhen", "count" : 2 }
我们将要分组的字段传递给$group函数的_id字段,然后每当查到一个,就给count加1,这样就可以统计出每个城市的订单数量
算术操作符
通过算术操作符我们可以对分组后的文档进行求和或者求平均数。比如我想计算每个城市订单运费总和,如下:
> db.user.aggregate({$group:{_id:"$orderAddressL",totalFreight:{$sum:"$freight"}}})
{ "_id" : "ShenZhen", "totalFreight" : 33 }
计算每个城市运费的平均数:
> db.user.aggregate({$group:{_id:"$orderAddressL",avgFreight:{$avg:"$freight"}}})
{ "_id" : "ShenZhen", "avgFreight" : 16.5 }
极值操作符
极值操作符用来获取分组后数据集的边缘值,比如获取每个城市最贵的运费,如下:
> db.user.aggregate({$group:{_id:"$orderAddressL",maxFreight:{$max:"$freight"}}})
{ "_id" : "ShenZhen", "maxFreight" : 20 }
查询每个城市最便宜的运费:
> db.user.aggregate({$group:{_id:"$orderAddressL",minFreight:{$min:"$freight"}}})
{ "_id" : "ShenZhen", "minFreight" : 13 }
按城市分组之后,获取该城市第一个运费单:
> db.user.aggregate({$group:{_id:"$orderAddressL",firstFreight:{$first:"$freight"}}})
{ "_id" : "ShenZhen", "firstFreight" : 13 }
获取分组后的最后一个运费单:
> db.user.aggregate({$group:{_id:"$orderAddressL",lastFreight:{$last:"$freight"}}})
{ "_id" : "ShenZhen", "lastFreight" : 20 }
$addToSet
可以将分组后的某一个字段放到一个数组中,但是重复的元素将只出现一次,而且元素加入到数组中的顺序是无规律的,比如将分组后的每个城市的运费放到一个数组中,如下:
> db.user.aggregate({$group:{_id:"$orderAddressL",freights:{$addToSet:"$freight"}}})
{ "_id" : "ShenZhen", "freights" : [ 13, 20 ] }
重复的freight将不会被添加进来
$push则对重复的数据不做限制,都可以添加进来,如下
db.user.aggregate({$group:{_id:"$orderAddressL",freights:{$push:"$freight"}}})
$unwind
$unwind
用来实现对文档的拆分,可以将文档中的值拆分为单独的文档,比如我的数据如下:
> db.user.find()
{ "_id" : ObjectId("6221a5b180900fc3a9cad278"), "name" : "鲁迅", "books" : [ { "name" : "呐喊", "publisher" : "花城出版社" }, { "name" : "彷徨", "publisher" : "南海出版出" } ] }
> db.user.aggregate({$unwind:"$books"})
{ "_id" : ObjectId("6221a5b180900fc3a9cad278"), "name" : "鲁迅", "books" : { "name" : "呐喊", "publisher" : "花城出版社" } }
{ "_id" : ObjectId("6221a5b180900fc3a9cad278"), "name" : "鲁迅", "books" : { "name" : "彷徨", "publisher" : "南海出版出" } }
$sort
> db.user.find()
{ "_id" : ObjectId("6221a6a580900fc3a9cad279"), "name" : "zhangsan", "age" : 18 }
{ "_id" : ObjectId("6221a6b180900fc3a9cad27a"), "name" : "lisi", "age" : 17 }
{ "_id" : ObjectId("6221a6bd80900fc3a9cad27b"), "name" : "wangwu", "age" : 19 }
$sort
操作可以对文档进行排序,如下:
> db.user.aggregate({$sort:{age:1}})
{ "_id" : ObjectId("6221a6b180900fc3a9cad27a"), "name" : "lisi", "age" : 17 }
{ "_id" : ObjectId("6221a6a580900fc3a9cad279"), "name" : "zhangsan", "age" : 18 }
{ "_id" : ObjectId("6221a6bd80900fc3a9cad27b"), "name" : "wangwu", "age" : 19 }
1表示升序、-1表示降序
可以按照存在的字段排序,也可以按照重命名的字段排序,如下:
> db.user.aggregate({$project:{oa:"$age"}},{$sort:{oa:-1}})
{ "_id" : ObjectId("6221a6bd80900fc3a9cad27b"), "oa" : 19 }
{ "_id" : ObjectId("6221a6a580900fc3a9cad279"), "oa" : 18 }
{ "_id" : ObjectId("6221a6b180900fc3a9cad27a"), "oa" : 17 }
$limit
$limit
返回结果中的前n个文档,如下表示返回结果中的前2个文档
> db.user.aggregate({$project:{oa:"$age"}},{$limit:2})
{ "_id" : ObjectId("6221a6a580900fc3a9cad279"), "oa" : 18 }
{ "_id" : ObjectId("6221a6b180900fc3a9cad27a"), "oa" : 17 }
$skip
$skip
表示跳过前n个文档,比如跳过前2个文档
> db.user.aggregate({$project:{oa:"$age"}},{$skip:2})
{ "_id" : ObjectId("6221a6bd80900fc3a9cad27b"), "oa" : 19 }