MongoDB的聚合框架Aggregation Framework入门学习教程
1.聚合框架
使用聚合框架对集合中的文档进行变换和组合,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering),投射(projecting),分组(grouping),排序(sorting),限制(limiting),跳过(skipping)。
例如一个保存着动物类型的集合,希望找出最多的那种动物,假设每种动物被保存为一个mongodb文档,可以按照以下步骤创建管道。
1)将每个文档的动物名称映射出来。
2)安装名称排序,统计每个名称出现的次数。
3)将文档按照名称出现的次数降序排列。
4)将返回结果限制为前五个。
具体操作符:
1){"$porject",{"name":1}}
类似于查询阶段的字段选择器,指定"fieldname":1选定需要的字段,"fieldname":0排除不需要的字段,"_id"字段自动显示。结果保存在内存中,不会写入磁盘。
db.test_collection.aggregate({"$project":{"name":1}});=>
{"_id":ObjectId("535a2d3c169097010b92fdf6"),"name":"snake"}
2){"$group",{"_id":"$name","count":{"$sum":1}}}
首先指定了分组的字段"name",该操作执行完后,每个name只对应一个结果,所有可以将name指定为唯一标识符"_id"。
第二个字段表明分组内的每个文档"count"字段加1。新加入的文档中不会有count字段。
db.test_collection.aggregate({"$project":{"name":1}},{"$group":{"_id":"$name","count":{"$sum":1}}});=>
{"_id":"bird","count":8344}
{"_id":"snake","count":8443}
{"_id":"cat","count":8183}
{"_id":"rabbit","count":8206}
{"_id":"tiger","count":8329}
{"_id":"cow","count":8309}
{"_id":"horse","count":8379}
{"_id":"dog","count":8406}
{"_id":"dragon","count":8372}
{"_id":"elephant","count":8264}
{"_id":"pig","count":8403}
{"_id":"lion","count":8362}
3){"$sort":{"count":-1}}
对结果集中的文档根据count字段做降序排列。
4){"$limit":5}
将返回结果限制为5个文档。
将上述结果综合起来:
db.test_collection.aggregate(
{
"$project":{"name":1}},
{"$group":{"_id":"$name","count":{"$sum":1}}},
{"$sort":{"count":-1}},
{"$limit":5}
);
aggregate会返回一个文档数组,内容为出现次数最多的5个动物:
{"_id":"snake","count":8443}
{"_id":"dog","count":8406}
{"_id":"pig","count":8403}
{"_id":"horse","count":8379}
{"_id":"dragon","count":8372}
调试过程中。可以逐一对管道符进行排查。
聚合框架不能对集合进行写入操作,所有结果返回给客户端,聚合结果必须限制在16M以内。
2.管道操作符
每个操作符都会接受一连串的文档,对这些文档进行类型转换,最后得到的文档作为结果传递给下一操作符。
不同的管道操作符可以将任意顺序组合在一起使用,而且可以被重复任意多次。
2.1$match
$match用于对文档集合进行筛选,之后得到的文档子集做聚合。
"$match"支持所有的常规查询操作符("$gt","$lt","$ne")等,不能使用地理空间操作符。
实际操作中尽量将"$match"放在管道的前面部分,一方面可以提快速将不需要的文档过滤掉,另外在映射和分组前筛选,查询可以使用索引。
2.2$project
使用"$project"可以提取字段,可以重命名字段,
db.foo.aggregate({"$project":{"city":1,"_id":0}})=>
{"city":"NEWWORK"}
可以将投射过的字段重命名:
db.foo.aggregate({"$project":{"newcity":"$city","_id":0}})=>
{"newcity":"NEWWORK"}
使用"$fieldname"语法为了在聚合框架中引用fieldname字段,例如上面"$city"会被替换为"NEWWORK"。
对字段重命名后,Mongdb不会记录其记录字段的历史名称,所以应该在修改字段名称前使用索引。
2.2.1管道表达式
可以使用表达式将多个字面量和变量组合为一个值。
可以使用组合或者任意深度的嵌套,创建复杂的表达式。
2.2.2数学表达式
数学表示式用来操作数据运算。
db.foo.aggregate(
{"$project":
{"total":
{"$add":["$age","$year"]},
"_id":0
}
}
)
{"total":15}
可以将多个表达式组合为更为复杂的表达式:
db.foo.aggregate(
{"$project":
{"sub":
{"$subtract":[{"$add":["$age","$year"]},7]},
"_id":0
}
}
)
{"sub":8}
操作符语法:
1)"$add":[expr1,[,expr2,...,exprN]]
将表达式相加
2)"$subtract":[expr1,expr2]
表达式1减去表达式2
3)"$multiply":[expr1,[,expr2,...,exprN]]
将表达式相乘
4)"$divide":[expr1,expr2]
表达式1除以表达式2得到商
5)"$mod":[expr1,expr2]
表达式1除以表达式2得到余数
2.2.3日期表达式
用于提取日期信息的表达式:"$year","$month","$week","$dayOfMonth","$dayOfweek","$hour","$minute","$second"。只能对日期类型的字段进行日期操作,不能对数值类型进行日期操作。
db.bar.insert({"name":"pipi","date":newDate()})
db.bar.aggregate(
{"$project":
{"birth-month":
{"$month":"$date"},
"_id":0
}
}
)
{"birth-month":4}
也可以使用字面量日期。
db.bar.aggregate(
{"$project":
{"up-to-now":
{"$subtract":[{"$minute":newDate()},{"$minute":"$date"}]},
"_id":0
}
}
)
{"up-to-now":18}
2.2.3字符串表达式
操作符语法:
1)"$substr":[expr,startOffset,numoReturn]
接受字符串,起始位置以后偏移N个字节,截取字符串。
2)"$concat":[expr1[,expr2,...,exprN]]
将给定的表达式连接在一起作为返回结果。
3)"$toLower":expr
返回参数的小写形式
4)"$toUpper":expr
返回参数的大写形式
例如:
db.foo.insert({"firstname":"caoqing","lastname":"lucifer"})
db.foo.aggregate(
{
"$project":{
"email":{
"$concat":[
{"$substr":["$firstname",0,1]},
".",
"$lastname",
"@gmail.com"
]
},
"_id":0
}
}
)
{"email":"c.lucifer@gmail.com"}
2.2.3逻辑表达式
操作符语法:
1)"$cmp":[expr1,expr2]
比较两个参数,相等返回0,大于返回整数,小于返回负数。
2)"$strcasecmp":[string1,string2]
比较字符串,区分大小写
3)"$eq"/"$ne"/"$gt"/"$gte"/"lt"/"lte":[expr1,expr2]
比较字符串,返回结果(trueorfalse)
4)"$and":[expr1[,expr2,...,exprN]]
所有值为true返回true,否则返回false。
5)"$or":[expr1[,expr2,...,exprN]]
任意表达式为true返回true,否则返回false
6)"$not":expr
对表示式取反
还有两个控制语句。
"$crond":[booleanExpr,trueExpr,falseExpr]
如果为true,返回trueExpr,否则,返回falseExpr。
"$ifFull":[expr,replacementExpr]
如果expr为null,返回replacementExpr,否则返回expr。
算术操作符必须接受数值,日期操作符必须接受日期,字符串操作符必须接受字符串。
例如,根据学生出勤率(10%),平时作业(30%)和考试成绩(60%)得出最终成绩,如果是老师宠爱的学生,直接得100分:
插入数据:
db.bar.insert(
{
"name":"xiaobao",
"teachersPet":1,
"attendance":90,
"quizz":80,
"test":85
}
)
db.bar.insert(
{
"name":"caoqing",
"teachersPet":0,
"attendance":20,
"quizz":50,
"test":90
}
)
db.bar.insert(
{
"name":"pipi",
"teachersPet":0,
"attendance":100,
"quizz":50,
"test":10
}
)
聚合:
db.bar.aggregate(
{
"$project":{
"grade":{
"$cond":[
"$teachersPet",
100,
{
"$add":[
{"$multiply":[0.1,"$attendance"]},
{"$multiply":[0.3,"$quizz"]},
{"$multiply":[0.6,"$test"]},
]
}
]
},
"_id":0
}
}
)
返回结果:
{"grade":100}
{"grade":71}
{"grade":31}
3.MapReduce
Mapreduce非常强大与灵活,Mongodb使用javascript作为查询语言,可以表示任意复杂的逻辑。
Mapreduce非常慢,不应该用在实际的数据分析中。
Mapreduce可以在多台服务器之间并行执行,可以将一个问题拆分为多个小问题,之后将各个小问题发送到不同的机器上,每台机器只负责完成一部分工作,所有的机器完成时,将这些零碎的解决方案合并为一个完整的解决方案。
最开始的是映射(map),将操作映射到集合中的各个文档,然后是中间环节,成为洗牌(shuffle),按照键分组,将产生的键值组成列表放在对应的键中。化简(reduce)则是把列表中的值化简为一个单值。
3.1找出集合中的所有键
MongoDB假设你的模式是动态的,所以并不会跟踪记录每个文档的键。通常找到集合中所有文档的所有键的最好方式就是MapReduce。
在映射环节,map函数使用特别的emit函数返回要处理的值。emit会给MapReduce一个键和一个值。
这里用emit将文档某个键的计数返回。this就是当前映射文档的引用:
map=function(){
emit(this.country,{count:1});
}
reduce接受两个参数,一个是key,就是emit返回的第一个值,还有一个数组,由一个或多个键对应的{count:1}文档组成。
reduce=function(key,value){
varresult={count:0};
for(vari=0;i<value.length;i++){
result.count+=value[i].count;
}
returnresult;
}
示例表数据:
{"_id":38,"country":"japan","money":724}
{"_id":39,"country":"germany","money":520}
{"_id":40,"country":"india","money":934}
{"_id":41,"country":"china","money":721}
{"_id":42,"country":"germany","money":156}
{"_id":43,"country":"canada","money":950}
{"_id":44,"country":"india","money":406}
{"_id":45,"country":"japan","money":776}
{"_id":46,"country":"canada","money":468}
{"_id":47,"country":"germany","money":262}
{"_id":48,"country":"germany","money":126}
{"_id":49,"country":"japan","money":86}
{"_id":50,"country":"canada","money":870}
{"_id":51,"country":"india","money":98}
{"_id":52,"country":"india","money":673}
{"_id":53,"country":"japan","money":487}
{"_id":54,"country":"india","money":681}
{"_id":55,"country":"canada","money":491}
{"_id":56,"country":"japan","money":98}
{"_id":57,"country":"china","money":172}
运行结果:
db.foo.mapReduce(map,reduce,{out:"collection"})
{
"result":"collcetion",
"timeMillis":83,
"counts":{
"input":99,
"emit":99,
"reduce":5,
"output":5
},
"ok":1,
"$gleStats":{
"lastOpTime":Timestamp(1399168165,15),
"electionId":ObjectId("535a2ce15918f42de9ab1427")
},
}
(1)result:存放的集合名
(2)timeMillis:操作花费的时间,单位是毫秒
(3)input:传入文档数目
(4)emit:此函数被调用的次数
(5)reduce:此函数被调用的次数
(6)output:最后返回文档的个数
查看下collection结果内容:
db.collection.find();
{"_id":"canada","value":{"count":19}}
{"_id":"china","value":{"count":15}}
{"_id":"germany","value":{"count":25}}
{"_id":"india","value":{"count":20}}
{"_id":"japan","value":{"count":20}}
3.2MapRecude其他的键
(1)"finalize":function
可以将reduce的结果发送给这个键,这是整个处理过程的最后一步。
(2)"keeptemp自动为true。":boolean
如果为true,则在连接关闭后结果保存,否则不保存。
(3)"out":string
输出集合的名称,如果设置,keeptemp自动为true。
(4)"query":document
在发往map前,先用指定条件过滤文档。
(5)"sort":document
在发往map前,先进行排序。
(6)"limit":integer
发往map函数的文档数量上限。
(7)"scope":document
可以在javascripts代码中使用的变量。
(8)"verbose":boolean
是否记录详细的服务器日志。
3.2.1finalize函数
可以使用finalize函数作为参数,会在最后一个reduce输出结果后执行,然后将结果保存在临时集合里。
3.2.2保存结果集合
默认情况下,执行mapreduce时创建一个临时集合,集合名称为mr.stuff.ts.id,即mapreduce.集合名.时间戳.数据库作业ID。MongoDB会在调用的连接关闭时自动销毁这个集合。
3.2.3对子文档执行mapreduce
每个传递给map的文档都需要先反序列化,从BSON对象转换为js对象,这个过程非常耗时,可以先对文档过滤来提高map速度,可以通过"query","limit"和"sort"等对文档进行过滤。
"query"的值是一个查询文档。
"limit","sort"配合可以发挥很大的作用。
"query","limit"和"sort"可以随意组合使用。
3.2.4作用域
作用域键"scope",可以用变量名:值这样普通的文档来设置该选项,
3.2.5获取更多的输出
设置verbose为true,可以将mapreduce过程更多的信息输出到服务器日志上。
4聚合命名
count和distinct操作可以简化为普通命令,不需要使用聚合框架。
4.1count
count返回集合中的文档数量:
db.foo.count()=> 99
可以传入一个查询文档:
db.foo.count({country:"china"})=>
15
增加查询条件会使count变慢。
4.2distinct
distinct用来找出给定键的所有不同值。使用时必须指定集合和键。
db.runCommand({"distinct":"foo","key":"country"})=>
{
"values":[
"japan",
"germany",
"india",
"china",
"canada"
],
"stats":{
"n":99,
"nscanned":99,
"nscannedObjects":99,
"timems":22,
"cursor":"BasicCursor"
},
"ok":1,
"$gleStats":{
"lastOpTime":Timestamp(1399171995,15),
"electionId":ObjectId("535a2ce15918f42de9ab1427")
}
}
4.3group
使用group可以进行更为复杂的聚合。先选定分组所依据的键,然后根据选定键的不同值分为若干组,然后对每一个分组进行聚合,得到结果文档。
插入示例数据:
varname=["Caoqing","Spider-man","Garfield"]
for(vari=0;i<10000;i++){
iname=name[Math.floor(Math.random()*name.length)];
date=newDate().getTime();
number=Math.floor(100*Math.random());
db.coll.insert({_id:i,name:iname,time:date,age:number});
}
生成的列表中包含最新的时间和最新的时间对应的年纪。
可以安装name进行分组,然后取出每个分组中date最新的文档,将其加入结果集。
db.runCommand({"group":{
"ns":"coll",
"key":{"name":true},
"initial":{"time":0},
"$reduce":function(doc,prev){
if(doc.time>prev.time){
prev.age=doc.age;
prev.time=doc.time;
}
}
}})
(1)"ns":"coll"
指定进行分组的集合。
(2)"key":{"name":true}
指定分组依据的键。
(3)"initial":{"time":0}
初始化time值,作为初始Wednesday传递给后续过程。每组成员都会使用这个累加器。
结果:
"$reduce":function(doc,prev){...}
{
"retval":[
{
"name":"Spider-man",
"time":1399179398567,
"age":55
},
{
"name":"Garfield",
"time":1399179398565,
"age":85
},
{
"name":"Caoqing",
"time":1399179398566,
"age":86
}
],
"count":10000,
"keys":3,
"ok":1,
"$gleStats":{
"lastOpTime":Timestamp(1399179362,1),
"electionId":ObjectId("535a2ce15918f42de9ab1427")
}
}
如果有文档不存在指定分组的键,这些文档会单独分为一组,缺失的键会使用name:null这样的形式。如下:
db.coll.insert({age:5,time:newDate().getTime()})
返回结果:
...
{
"name":null,
"time":1399180685288,
"age":5
}
"count":10001,
"keys":4,
...
为了排除不包含指定用于分组的键的文档,可以在"condition"中加入"name":{"$exists":true}。
db.runCommand({"group":{
"ns":"coll",
"key":{"name":true},
"initial":{"time":0},
"$reduce":function(doc,prev){
if(doc.time>prev.time){
prev.age=doc.age;
prev.time=doc.time;
}
},
"condition":{"name":{"$exists":true}}
}})
4.3.1使用完成器
完成器(finalizer)用于精简从数据库传到用户的数据,因为group命令的输出结果需要能够通过单次数据库响应返回给用户。
4.3.2将函数作为键使用
分组条件可以非常复杂,不是单个键,例如分组时按照类别分组dog和DOG是两个完全不同的组,为了消除大小写差异,可以定义一个函数决定文档分组所依据的键。
定义分组函数需要用到"$keyf"键,
db.foo.group({
"ns":"foo",
"$keyf":function(x){returnx.category.toLowerCase();};
"initial":...,
......
})