Mongodb聚合(三) 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) { var result = {count : 0}; for (var i = 0; i < value.length; i++) { result.count += value[i].count; } return result; } 示例表数据: { "_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 } Type "it" for more 运行结果: 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") }, } result:存放的集合名 timeMillis:操做花费的时间,单位是毫秒 input:传入文档数目 emit:此函数被调用的次数 reduce:此函数被调用的次数 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.2 MapRecude其余的键 "finalize" : function 能够将reduce的结果发送给这个键,这是整个处理过程的最后一步。 "keeptemp自动为true。" : boolean 若是为true,则在链接关闭后结果保存,不然不保存。 "out" : string 输出集合的名称,若是设置,keeptemp自动为true。 "query" : document 在发往map前,先用指定条件过滤文档。 "sort" : document 在发往map前,先进行排序。 "limit" : integer 发往map函数的文档数量上限。 "scope" : document 能够在javascripts代码中使用的变量。 "verbose" : boolean 是否记录详细的服务器日志。 3.2.1 finalize函数 可使用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.1 count count返回集合中的文档数量: db.foo.count() => 99 能够传入一个查询文档: db.foo.count({country : "china"}) => 15 增长查询条件会使count变慢。 4.2 distinct 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.3 group 使用group能够进行更为复杂的聚合。先选定分组所依据的键,而后根据选定键的不一样值分为若干组,而后对每个分组进行聚合,获得结果文档。 插入示例数据: var name = ["Caoqing", "Spider-man", "Garfield"] for (var i = 0; i < 10000; i++) { iname = name[Math.floor(Math.random() * name.length)]; date = new Date().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; } } }}) "ns" : "coll" 指定进行分组的集合。 "key" : {"name" : true} 指定分组依据的键。 "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 : new Date().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) { return x.category.toLowerCase(); }; "initial" : ..., ...... })