一、引言
好久没有写东西了,MongoDB系列的文章也丢下好长时间了。今天终于有时间了,就写了一篇有关聚合的文章。一说到“聚合”,用过关系型数据库的人都应该知道它是一个什么东西。关系型数据库有“聚合”的概念,我们的MongoDB数据库也有聚合的概念,今天我就来抛砖引玉,简单的说一说MongoDB文档数据库中的“聚合”的概念。
二、简介
管道是MongoDB2.2版本引入新的功能 ,它是数据聚合的一个新框架,其概念类似于数据处理的管道。 每个文档通过一个由多个节点组成的管道,每个节点有自己特殊的功能(分组、过滤等),文档经过管道处理后,最后输出相应的结果。管道基本的功能有两个:一是对文档进行“过滤”,也就是筛选出符合条件的文档;二是对文档进行“变换”,也就是改变文档的输出形式。其他的一些功能还包括按照某个指定的字段分组和排序等。而且在每个阶段还可以使用表达式操作符计算平均值和拼接字符串等相关操作。管道提供了一个MapReduce 的替代方案,MapReduce使用相对来说比较复杂,而管道拥有固定的接口(操作符表达),使用比较简单,对于大多数的聚合任务管道一般来说是首选方法。
三、管道的相关操作
管道操作符作为“键”,所对应的“值”叫做管道表达式。它可以看做是管道操作符的操作数(Operand),每个管道表达式是一个文档结构,它是由字段名、字段值、和一些表达式操作符组成的,
每个管道表达式只能作用于处理当前正在处理的文档,而不能进行跨文档的操作。管道表达式对文档的处理都是在内存中进行的。除了能够进行累加计算的管道表达式外,其他的表达式都是无状态的,也就是不会保留上下文的信息。累加性质的表达式操作符通常和$group操作符一起使用,来统计该组内最大值、最小值等,例如上面的例子中我们在$group管道操作符中使用了具有累加的$sum来计算总和。
1、管道表达式使用详解
语法:aggregate() 方法的基本语法格式如下所示:
>db.collectionName.aggregate(aggregateOperation)
参数说明:
collectionName:该参数表示的是集合的名称,就是在哪个集合上进行聚合操作。
aggregateOperation:具体索要执行的聚合操作的名称
下面列出了一些聚合的管道表达式:
表达式 描述 实例
$sum 计算总和。 db.users.aggregate([{$group : {_id : "$author",num_tutorial : {$sum : "$likes"}}}])
$avg 计算平均值 db.users.aggregate([{$group : {_id : "$author",num_tutorial : {$avg : "$likes"}}}])
$min 获取集合中所有文档对应值得最小值。 db.users.aggregate([{$group : {_id : "$author",num_tutorial : {$min : "$likes"}}}])
$max 获取集合中所有文档对应值得最大值。 db.users.aggregate([{$group : {_id : "$author",num_tutorial : {$max : "$likes"}}}])
$push 在结果文档中插入值到一个数组中。 db.users.aggregate([{$group : {_id : "$author",url : {$push: "$url"}}}])
$addToSet 在结果文档中插入值到一个数组中,但不创建副本。 db.users.aggregate([{$group : {_id : "$author",url : {$addToSet : "$url"}}}])
$first 根据资源文档的排序获取第一个文档数据。 db.users.aggregate([{$group : {_id : "$author",first_url : {$first : "$url"}}}])
$last 根据资源文档的排序获取最后一个文档数据 db.users.aggregate([{$group : {_id : "$author",last_url : {$last : "$url"}}}])
1.1、【 $sum】 的用法
1 //原始数据: 2 > db.users.find() 3 { "_id" : ObjectId(5b1e044733091e826f7c2c74"),title" : MongoDb Overview",1)">descriptionMongodb is not sql databaseauthorhuangFeiHongurlhttp://www.huangfeihong.comtags" : [ mongodbdatabaseNosql" ],1)">likes100 } 4 { 5b1e044733091e826f7c2c75Nosql OverviewNo sql database is very fast10 5 { 5b1e044733091e826f7c2c76Log4Net Overrivewlog4net is not sql databaselingChonghttp://www.linchong.comlognetNosql750 6 > 7 8 9 1】、类似sql语句: select author,count(*) from users group by author 10 11 > db.users.aggregate([{$group:{":$authorbooks":{$sum:1}}}]); 12 { 13 { 214 15 16 17 2】、类似sql语句: select count(1) as count users 18 19 > db.users.count(); 20 3 21 22 > db.users.aggregate([{$group:{":null,count:{$sum23 { " : count324 25 26 27 3】、类似sql语句: select sum(likes) as total 28 29 > db.users.aggregate([{$group:{total":{$likes"}}}]) 30 { 86031 32 33 类似sql语句: 34 35 > db.users.aggregate([{$group:{36 { 37 { 110 }
1.2、【 $max】 的用法
原始数据: > db.users.find() { } { } > users group by author,我们通过相同的作者来进行分组,然后查询相同作者的书最多的阅读详情 > db.users.aggregate([{$group:{max$max}}}]); { } { 100 }
1.3、【 $min】 的用法
users group by author,我们通过相同的作者来进行分组,然后查询相同作者的书最少的阅读详情 > db.users.aggregate([{$group:{min$min10 }
1.4、【 $avg】 的用法
as average users group by author,我们可以根据相同的作者分类算出相同作者阅读数量的平均值 > db.users.aggregate([{$group:{average$avg}}}]) { 55 } select avg(likes) users > db.users.aggregate([{$group:{286.6666666666667 } >
1.5、【 $push】 的用法,将指定的表达式的值添加到一个数组中,就算是有重复的值也会显示,这个值不要超过16M,不然会出现错误
100,1)">quantity410,1)">12Log4Net OverviewlinChong750,1)">65b1e1bfb33091e826f7c2c77MongoDb Higher120,1)">235b1e1bfb33091e826f7c2c78Nosql Redis Overviewredis30,1)">335b1e1bfb33091e826f7c2c79Memcached OverrivewMemcached is sql databasewuSonghttp://www.wusong.commemcachedcache50,1)">】、我们通过相同的作者来进行分组,然后查询每个相同作者卖出的数量放在数组里面 db.users.aggregate({$group:{quantities$push$quantity}}}); { ] } { 6,1)">4,1)">12,1)"> ] } 】、我们通过相同的作者来进行分组,然后查询每个相同作者卖出的数量放在数组里面,并且显示书名和其销量 > db.users.aggregate({$group:{$title}}}}); { " : [ { } ] } { " },{ 33,1)">23,1)"> } ] } > 3】、我们通过相同的作者来进行分组,然后查询每个相同作者卖出的数量放在数组里面,并且显示书名和其销量,有重复值,也会显示【{ " },{ }】 > db.users.aggregate({$group:{ } ] } >
1.6、【 $addToSet】 的用法,将表达式的值添加到一个数组中(无重复值),这个值不要超过16M,不然会出现错误
} 1】、我们通过相同的作者来进行分组,然后查询每个相同作者卖出的数量放在数组里面,有重复值,只显示一个4,【{ ] } 】 > db.users.aggregate({$group:{$addToSet ] } } ] } >
1.7、【 $first】 的用法,返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。
} > 】、按作者进行分组,获取分组的第一条记录 > db.users.aggregate({$groupquantityFrist$first}}}) { 4 }
1.8、【 $last】 的用法,返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。
} > 】、按作者进行分组,获取分组的最后一条记录 > db.users.aggregate({quantityLast$last4 }
2、管道操作符使用详解
管道是由一个个功能节点组成的,这些节点用管道操作符来进行表示。聚合管道以一个集合中的所有文档作为开始,然后这些文档从一个操作节点流向下一个节点 ,每个操作节点对文档做相应的操作。这些操作可能会创建新的文档或者过滤掉一些不符合条件的文档,在管道中可以对文档进行重复操作。管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。 MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。
2.1、【$project】:数据投影,主要用于重命名、增加和删除字段,也可以用于创建计算结果以及嵌套文档。
原始数据 > db.users.find({},{_id:0}) { } > 隐藏_id字段,并且只显示title和author字段 > db.users.aggregate({$project0,1)">1,1)">}}); { " }
2.2、【$match】:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。$match的语法和查询表达式(db.collection.find())的语法相同
} > 获取销售数量大于4并且小于等于12的作者,并按作者分组统计 > db.users.aggregate([{$match$gt$lte12}}},{}}}]); { } { } 获取销售数量大于4并且小于等于33的作者,并按作者分组统计 > db.users.aggregate([{33}}},1)">}}}]); { } { } > 获取销售数量大于等于4并且小于等于33的作者,并按作者分组统计 > db.users.aggregate([{$gte3 }
注意: 1、不能在$match操作符中使用$where表达式操作符。
2、$match尽量出现在管道的前面,这样可以提早过滤文档,加快聚合速度。
3、如果$match出现在最前面的话,可以使用索引来加快查询。
2.3、【$limit】:用来限制MongoDB聚合管道返回的文档数。
只显示前两条 > db.users.aggregate({$limit } 只显示前两条,并且隐藏_id字段 > db.users.aggregate({2},1)">}}) { 只显示前四条,并且隐藏_id字段 > db.users.aggregate({4},1)"> } >
2.4、【$skip】:在聚合管道中跳过指定数量的文档,并返回余下的文档。
跳过前2条,从第三条记录开始显示 > db.users.aggregate({$skip跳过前2条,从第三条记录开始显示,并结合$project 隐藏_id字段 > db.users.aggregate({通过$skip和$limit分页显示,显示第一页,每页显示2条记录,并隐藏_id和url字段 > db.users.aggregate({0},url:通过$skip和$limit分页显示,显示第二页,每页显示2条记录,并隐藏_id和url字段 > db.users.aggregate({ } >
2.5、【$unwind】:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
把数组tags字段独立开来,每个数组元素是一条独立的记录,这是全部字段都显示的 > db.users.aggregate({$unwind$tags把数组tags字段独立开来,每个数组元素是一条独立的记录,这里进行了字段的减少 > db.users.aggregate({"},1)">10 }
注意:1、$unwind:"$tags"})不要忘了$符号
2、如果$unwind目标字段不存在的话,那么该文档将被忽略过滤掉,例如:
> db.article.aggregate({$project:{author:1,title:1,tags:1}},{$unwind:"$tag"})
{ "result" : [ ],"ok" : 1 }
将$tags改为$tag因不存在该字段,该文档被忽略,输出的结果为空
3、如果$unwind目标字段不是一个数组的话,将会产生错误,例如:
> db.article.aggregate({$project:{author:1,{$unwind:"$title"})
Error: Printing Stack Trace
at printStackTrace (src/mongo/shell/utils.js:37:15)
at DBCollection.aggregate (src/mongo/shell/collection.js:897:9)
at (shell):1:12
Sat Nov 16 19:16:54.488 JavaScript execution Failed: aggregate Failed: {
"errmsg" : "exception: $unwind: value at end of field path must be an array",
"code" : 15978,1)"> "ok" : 0
} at src/mongo/shell/collection.js:L898
4、如果$unwind目标字段数组为空的话,该文档也将会被忽略。
2.6、【$group】:将集合中的文档分组,可用于统计结果。
注意: 1、$group的输出是无序的。
2、$group操作目前是在内存中进行的,所以不能用它来对大量个数的文档进行分组。
2.7、【$sort】:将输入文档排序后输出。
根据quantity字段按升序排列 > db.users.aggregate([{$sort1}},1)">}}]); { } 根据quantity字段按降序排列 > db.users.aggregate([{":- } >
注意:1. 如果将$sort放到管道前面的话可以利用索引,提高效率
2. MongoDB 24.对内存做了优化,在管道中如果$sort出现在$limit之前的话,$sort只会对前$limit个文档进行操作,这样在内存中也只会保留前$limit个文档,从而可以极大的节省内存
3. $sort操作是在内存中进行的,如果其占有的内存超过物理内存的10%,程序会产生错误
2.8、【$goNear】:$goNear会返回一些坐标值,这些值以按照距离指定点距离由近到远进行排序
原始数据 > db.geoInstance.insert([{loc:[3]},{loc:[3,1)">4]},-2]},{loc:{x:9,y:5}},{loc:{lng:-9.2,lat:21.3}}]) BulkWriteResult({ writeErrors : [ ],writeConcernErrorsnInserted,1)">nUpsertednMatchednModifiednRemovedupserted : [ ] }) > db.geoInstance.find() { 5b1f4d21cf30d7cba03d5e03loc5b1f4d21cf30d7cba03d5e045b1f4d21cf30d7cba03d5e055b1f4d21cf30d7cba03d5e06" : [ -5b1f4d21cf30d7cba03d5e07" : { xy5 } } { 5b1f4d21cf30d7cba03d5e08lng" : -lat } } > 注意:一定要建立2d索引,否则不能计算距离(我在这里掉坑了) > db.geoInstance.ensureIndex({2d}); { createdCollectionAutomaticallyfalsenumIndexesBeforenumIndexesAfterok } 执行结果 > db.geoInstance.aggregate({$geoNear":{near:[2],distanceField:distance}}) { 5 },1)">3.16227766016837954 ],1)">7.2801098892805183 ],1)">9.05538513813741711.1803398874989492 ],1)">1621.3 },1)">27.223702907576698 } >
注意: 1、使用$goNear只能在管道处理的开始第一个阶段进行
2、必须指定distanceField,该字段用来决定是否包含距离字段
3、$gonNear和geoNear命令比较相似,但是也有一些不同:distanceField在$geoNear中是必选的,而在geoNear中是可选的;includeLocs在$geoNear中是string类型,而在geoNear中是boolen类型。
五、结束
好了,就是这些了。学习是一个渐进的过程,今天只是初步涉及“聚合”的操作,还有有差距,有些概念要好好的理解一下。不忘初心,继续努力吧。