在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集合中的数据。