目录
一.聚合&管道
1.操作
2.例子
3.提高管道性能
二.Map/Reduce
聚合是MongoDB的高级查询框架,实际上在MySQL等关系数据库中,也有GROUP BY这样的类似功能。其主要作用是,从多个文档中提取、转换和整合数据,形成新的信息,可以用来发现文档间的一些关系,或者挖掘单个文档不具备的信息。例如,春节快到了,如果一家商店的店长想统计每月销售额、每种商品销售额、整年销售额,就必须以时间或商品ID作为分组条件进行统计。MongoDB提供了聚合和MapReduce两种工具,聚合要简单些。
管道是计算机领域一个很普遍的概念,指的是对于一系列操作,前一个操作的结果通过管道输送给后一个操作,作为其输入。一个典型例子是linux的管道,通过配合grep、awk等工具,可以很方便的从命令行输出中提取出需要的信息。
MongoDB有如下管道操作:
聚合管道的使用形式为:
db.collection_name.aggregate(pipeline,options)
有如下选项:
下面举一些例子,先展示下user集合的数据,一共6条:
> db.user.find()
{ "_id" : ObjectId("5c3eef6d7da85af675c7c107"), "name" : "zhangsan", "sex" : "man", "age" : 21, "hobby" : "programming" }
{ "_id" : ObjectId("5c3eefee7da85af675c7c108"), "name" : "lisi", "sex" : "woman", "age" : 16, "hobby" : "music" }
{ "_id" : ObjectId("5c3ef0037da85af675c7c109"), "name" : "wangwu", "sex" : "man", "age" : 18, "hobby" : "read book" }
{ "_id" : ObjectId("5c3f1885cce0b679769390fa"), "name" : "lucy", "age" : 22, "hobby" : "movie", "sex" : "woman" }
{ "_id" : ObjectId("5c3f1bbfcce0b67976939109"), "name" : "tom", "age" : 2 }
{ "_id" : ObjectId("5c3f22a87da85af675c7c10a"), "name" : 10 }
1)首先统计每种性别的数量:
> db.user.aggregate({$group:{_id:'$sex',count:{$sum:1}}})
{ "_id" : null, "count" : 2 }
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }
_id就是用来指定分组依据,这里是sex域,注意需要带上美元符号$对于集合内每一个文档,如果sex字段相同,则分入同一组,反之亦然;$sum操作符的值(即1)代表每个组中每有一个文档,统计结果的"count"字段的值就加1。这个查询可以翻译成:select count(*) from user group by sex。可以看到,对于不存在的字段,也会作为null值参与统计。
2)假如只想统计有sex字段的文档,就可以用上$match操作符:
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} })
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }
可以看到,$match的用法和find()函数很像。另外,当aggregate方法中存在多个操作时(即管道有多个环节),按照从左到右顺序执行。
3)MongoDB为aggregate提供了一个forEach()方法,可接受一个JavaScript函数,并继续处理管道内的数据:
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} }).forEach(function(doc){if(doc.count%2 == 0) db.result.insert(doc);})
> db.result.find()
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }
这里使用的函数是,如果管道内的结果的count字段能整除2,则插入到result集合中,由于两个结果的count都是2,均满足条件,因此都插入了。
4)上例和以下语句作用相同:
> db.result.drop()
true
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$out:"result"})
> db.result.find()
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }
在这个管道中,首先过滤了没有sex域的文档,然后根据sex域的值进行分组,之后取count字段的值可以整除2的分组结果写入result集合。
5)如果不想看到_id字段,就可以用上$project了:
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$project:{_id:0}})
{ "count" : 2 }
{ "count" : 2 }
6)$project还可以用来改变字段名:
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$project:{"性别":"$_id","人数":"$count"}})
{ "_id" : "woman", "性别" : "woman", "人数" : 2 }
{ "_id" : "man", "性别" : "man", "人数" : 2 }
显然,_id字段不能被改名,对其使用$project相当于把该域的值复制一份再改名
由于$project具有投影的能力,因此也可以使用一些操作符,来实现诸如大小写转换、数学运算等操作:
管道虽然好用,但是以下因素会对其性能产生影响:
例如:对$match和$group分别explain如下(需要先建立索引)
$match:
> db.user.aggregate([{$match:{sex:{$exists:1}}}],{explain:true})
{
……
"indexName" : "sex_1",
……
}
可以看到,这里使用了索引
$group:
> db.user.aggregate([{$group:{_id:'$sex',count:{$sum:1}}}],{explain:true})
{
"stages" : [
{
"$cursor" : {
"query" : {
},
"fields" : {
"sex" : 1,
"_id" : 0
},
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.user",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "COLLSCAN",
"direction" : "forward"
},
"rejectedPlans" : [ ]
}
}
},
{
"$group" : {
"_id" : "$sex",
"count" : {
"$sum" : {
"$const" : 1
}
}
}
}
],
"ok" : 1
}
并没有使用索引。在数据量小、管道短的情况下,感觉上没有什么差距,不过一旦任务复杂起来,差距就会很明显了。因此,在使用管道前,最好对相关域建立索引,或者建立专用的索引域。
MapReduce是MongoDB另一个数据处理工具,来源是Google的论文,主要思想是 分治-聚合 ,即将大的任务分割为小的任务并行处理,然后将结果聚合在一起。主要用在分布式、大数据条件下。
函数原型为:
db.collection_name.mapReduce(map,reduce,option)
map、reduce是两个函数。map函数生成键值对序列,使用emit返回,作为 reduce 函数参数;reduce函数将key-values变成key-value,也就是把values数组变成一个单一的值value。
option:
示例如下:
> map = function(){emit(this.sex,1)}
function (){emit(this.sex,1)}
> reduce = function(k,v){return Array.sum(v)}
function (k,v){return Array.sum(v)}
> db.user.mapReduce(map,reduce,{query:{sex:{$exists:1}},out:"result"})
{
"result" : "result",
"timeMillis" : 115,
"counts" : {
"input" : 4,
"emit" : 4,
"reduce" : 2,
"output" : 2
},
"ok" : 1
}
> db.result.find()
{ "_id" : "man", "value" : 2 }
{ "_id" : "woman", "value" : 2 }
效果和聚合一节中第二个例子一样