MongoDB:mongodb的聚合和管道

x33g5p2x  于2022-03-05 转载在 Go  
字(15.1k)|赞(0)|评价(0)|浏览(620)

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 }

相关文章