MongoDB中使用MapReduce来进行聚合操作

来源:互联网 发布:2015java面试宝典下载 编辑:程序博客网 时间:2024/05/16 01:48

在mongoDB的MapReduce操作中,map函数产生一些列中间数据,这些中间数据是key/value的集合。reduce函数收集具有相同中间key值的value值,合并这些value值,形成一个较小的value值的集合。
一个MongDB的MapReduce执行的过程如下所示。
这里写图片描述
在这个MapReduce操作中,首先通过query筛选出了一部分的数据,然后对着一部分的数据进行map操作,输出了一些列中间值,这些中间值的key是cust_id,value是amount。最后对中间数据做reduce操作,产生了最终的结果。最终的结果是统计出了每一个cust_id对应的value值的总和。
在mongoDB中MapReduce有两种写法,一种是使用db.runCommand来执行MapReduce,一种是db.集合名.mapReduce()。两种方法效果一样。我们从第一种说起。
使用runCommand方式来执行一个MapReduce函数通常的模板如下所示:

db.runCommand(               {                 mapReduce: <collection>,                 map: <function>,                 reduce: <function>,                 finalize: <function>,                 out: <output>,                 query: <document>,                 sort: <document>,                 limit: <number>,                 scope: <document>,                 jsMode: <boolean>,                 verbose: <boolean>,                 bypassDocumentValidation: <boolean>,               }             )

对上面的每一个选项做一个说明:

字段 类型 描述 mapReduce collection 执行MapReduce操作的集合名,这个集合在执行map操作前,会先通过query来过滤集合中的数据 map function 一个JavaScript函数,将输入数据输出为一些列key/value的中间数据 reduce function 一个JavaScript函数,收集具有相同中间key值的value值,合并这些value值,形成一个较小的value值的集合。 out string或者document 指定将结果输出到哪里。 query document 可选的。在map函数操作之前指定过滤条件。 sort document 可选的。对输入的数据document数据排序。这个选项早做优化时很有用,比如将sort的key和emit的key设定为想相同,这样在reduce函数中就可以做更少的操作,极大提升效率。sort中的key必须有索引。 limit number 可选的。指定输入的document最大有多少条数据进入map函数。 finalize function 可选的。在reduce函数后执行,改变输出数据。 scope document 可选的。指定全局变量。这些变量可以在map,reduce和finalize函数中使用。 jsMode boolean 可选的。指定是否在map函数和reduce函数之间将中间结果转换成BSON格式的数据。如果是false,MongoDB将会将map函数输出的JavaScript对象转换成BSON对象。这些BSON对象在调用reduce函数是,会再被转换为JavaScript对象。map-reduce操作将中间的BSON对象临时的存放在磁盘中。这将允许map-reduce处理更大量的数据。如果为true,MongoDB将不会把map函数输出的JavaScript对象转换成BSON对象。这样能提高计算速度。在key少于500,000 时可以考虑将jsMode设置为true.jsMode默认是false. verbose Boolean 可选的。指定最终的输出信息中是否包含时间信息。默认是true,包含时间信息。 bypassDocumentValidation boolean 可选的。是否忽略文档检测。如果设置为true,将会允许你在插入document的时候进行必要的验证。

map Function

map函数将输入数据映射为一系列的key-value键。
map函数通常的样子就像下面这样:

function() {   ...//一些列处理   emit(key, value);}

以下是map函数中的一些使用注意事项:
在map函数中,如果要引用当前文档自身,可以使用this。
在任何情况下map函数不应该访问数据库。
map函数不应该对其他函数有副作用。
在一个map函数中可以任意多次调用emit函数来输出具有key/value形式的中间数据。
现在我们有一个集合goods。数据如下:

{ "_id" : ObjectId("5a02451e8f215943b6d22a78"), "gnum" : "001", "shelfnum" : "e1", "price" : "10" }{ "_id" : ObjectId("5a02451e8f215943b6d22a79"), "gnum" : "002", "shelfnum" : "e2", "price" : "20.3"}{ "_id" : ObjectId("5a0245538f215943b6d22a7a"), "gnum" : "003", "shelfnum" : "e1", "price" : "7.5" }{ "_id" : ObjectId("5a02456d8f215943b6d22a7b"), "gnum" : "004", "shelfnum" : "e2", "price" : "8.8" }{ "_id" : ObjectId("5a02457f8f215943b6d22a7c"), "gnum" : "005", "shelfnum" : "e3", "price" : "6.6" }

有如下map函数:

var map=function() {    emit(this.shelfnum, this.price);}

emit函数将文档中的shelfnum作为key,gnum作为value生成了一些列键值对。经过map函数,产生了下面的中间数据:

{"e1":["10","7.5"]}{"e2":["20.3","8.8"]}{"e3":"6.6"}

reduce Fuction

reduce函数大概看起来就想下面的样子:

function(key, values) {   ...//一系列处理过程   return result;}

reduce函数的注意点:
reduce函数不应该连接数据库。
reduce函数不应该影响外部系统的运行。
如果一个key只有一个value,那么MongoDB不回调用reduce函数。
对于同一个key,reduce函数可能会执行多次。
继续上面的例子

var reduce=function(key,values){   totalsum=0;   for(i=0;i<values.length;i++){    totalsum+=parseFloat(values[i]);   }   return totalsum;}

此时当执行MapReduce命令时,就会求出来每一个shelfnum对应商品价格的总和。

 db.runCommand({            mapReduce:"goods",//指定对哪个集合做操作            map:map,           //指定map函数            reduce:reduce,     //指定reduce函数            out:"goodsResult"   //指定输出结果到goodResult集合中            })

返回结果如下:

{        "result" : "goodsResult",        "timeMillis" : 280,        "counts" : {                "input" : 5,                "emit" : 5,                "reduce" : 2,                "output" : 3        },        "ok" : 1}

然后查询goodsResult集合就可以拿到我们计算出来的结果了。

 db.goodsResult.find(){ "_id" : "e1", "value" : 17.5 }{ "_id" : "e2", "value" : 29.1 }{ "_id" : "e3", "value" : "6.6" }

在MongoDB中, db.collection.mapReduce() 是db.runCommand的一个替代方案,可以直接对某个结合执行MapReduce操作。
orders集合中有如下结构的数据:

{     _id: ObjectId("50a8240b927d5d8b5891743c"),     cust_id: "abc123",     ord_date: new Date("Oct 04, 2012"),     status: 'A',     price: 25,     items: [ { sku: "mmm", qty: 5, price: 2.5 },              { sku: "nnn", qty: 5, price: 2.5 } ]}

我们要计算每一个顾客(cust_id)总的消费金额(price)。
定义map函数:

var mapFunction1 = function() {                       emit(this.cust_id, this.price);                   };

定义reduce函数:

var reduceFunction1 = function(keyCustId, valuesPrices) {                          return Array.sum(valuesPrices);                      };

执行MapReduce操作:

db.orders.mapReduce(                     mapFunction1,                     reduceFunction1,                     { out: "map_reduce_example" }                   )

这样就计算出了每一个顾客总的消费金额。
另外一个示例。

var mapFunction2 = function() {                       for (var idx = 0; idx < this.items.length; idx++) {                           var key = this.items[idx].sku;                           var value = {                                         count: 1,                                         qty: this.items[idx].qty                                       };                           emit(key, value);                       }                    };
var reduceFunction2 = function(keySKU, countObjVals) {                     reducedVal = { count: 0, qty: 0 };                     for (var idx = 0; idx < countObjVals.length; idx++) {                         reducedVal.count += countObjVals[idx].count;                         reducedVal.qty += countObjVals[idx].qty;                     }                     return reducedVal;                  };
var finalizeFunction2 = function (key, reducedVal) {                       reducedVal.avg = reducedVal.qty/reducedVal.count;                       return reducedVal;                    };
db.orders.mapReduce( mapFunction2,                     reduceFunction2,                     {                       out: { merge: "map_reduce_example" },                       query: { ord_date:                                  { $gt: new Date('01/01/2012') }                              },                       finalize: finalizeFunction2                     }                   )

上面这个例子中,map函数产生了类似{"mmm":[{count:1,qty: 5},...]}这样的中间数据。
reduce函数将每一个键值对中的键做了操作,计算出了count的总数和qty的总数。
finalize函数用reduce函数计算出的qty的总数除以count的总数,这样就计算出了每一个items的平均qty。当然这一步也可以直接在reduce函数里面做,省略finalize函数。

关于out的一些讨论

我们经常会遇到这样的问题,对一个集合做了MapReduce运算之后,这个集合进行了更新,此时我只想对新增的数据做MapReduce操作,而不想对整个集合再做一次MapReduce操作。并且操作后的结果需要加入到原来的集合中。这个时候我们可以通过query语句来过滤掉已经做过运算的数据,比如最常用的通过时间来过滤。在输出数据到集合时,通过out来指定是覆盖原来的集合还是将新结果合并到原来的结果。
我们上面的写法out: <collectionName>是直接生成一个新的集合,并且将结果输出到这个集合中。
完整的out看起来像下面这样:

out: { <action>: <collectionName>        [, db: <dbName>]        [, sharded: <boolean> ]        [, nonAtomic: <boolean> ] }

其中action可以指定一下几个值:

 • replace 替换原来的集合。 • merge 跟已经存在的集合进行合并,如果新旧集合有相同的key,用新的数据覆盖旧的数据。 • reduce 跟已经存在的集合进行合并,如果新旧集合有相同的key,对新旧文档执行reduce操作产生新的结果。

db:可选,数据库的名称。默认和输入集合使用同一个数据库。
sharded:boolean。可选。数据库有分片时使用。
nonAtomic:boolean。可选。只有在merge或者reduce时有效。默认是false。map-reduce操作的后续步骤会对数据库加锁。其他客户端无法访问到out集合中的数据。如果指定为true,在map-reduce操作的后续过程中,其他客户端就可以访问到out集合中的数据。