由于nodejs本身的限制,在程序中使用js进行大批量计算效率不高。而V8引擎自身对内存大小的限制(64位系统下1.4G),同样限制了数据规模。
因此,相对于从mongodb中抽出数据进行计算,在mongodb中利用聚合函数或者其他方法完成计算,避开nodejs自身限制的方案在可靠性和扩展性上都相对较为令人满意。
mongodb支持类似SQL中的聚合函数,虽然语法不通,不过基本原理类似。
mongodb自带的接口中,aggregate被用来实现聚合查询:
rec = db.LIBRARY.aggregate([{
$match: {
date: {
$gte: '2220832129000',
$lte: '3330918529000'
}
}
}, {
$group: {
_id: '$book_id',
maxScience: {
$max: '$shelf.science'
},
maxMath: {
$max: '$data.math'
},
avgAlgebra: {
$avg: '$data.algebra'
},
standardDeviation: {
$stdDevPop: '$data.score' // 标准差
},
allValues: {
$values: '$data.score' // 分组后的结果值数组
}
}
}])
在$group中可以利用各种运算符实现各种聚合运算。
如果目的是生成字段不多的简单数据结构,聚合运算几乎可以一步到位。
需要注意的是在没有格式转换的情况下,js对于字符串和数字的区分很模糊。如果对字符串变量使用max函数,出现的结果会是"999" > "1234"。如果mongodb内部数据格式不规范,可能得不到理想的结果。
对于复杂的计算,可以使用mapReduce方法。
mongodb的原生方法中包括了mapReduce方法,遍历所有符合query条件的数据,对于每一条抽取到的数据,通过map方法提出键值对,在收到的键发生变化时,将目前为止当前键的所有值存储在一个数组中。
对于reduce方法触发的时机,是在遍历结果集,键发生变化时触发,还是遍历结束后,对新的键值对遍历执行reduce方法,这个目前还不能确定。
传递给reduce方法的参数包括键和一个存储目前为止当前键的所有值的数组,reduce处理结束后,返回处理结果的统计结果,mapReduce过程结束。
mapReduce是个功能很强大的方法,目前我只能将接触到的内容记录下来,等待以后慢慢完善。
首先,是最核心的map方法和reduce方法。
※ map方法
处理query条件抽取的结果集,对于每一条mongodb的记录,都会执行一次map方法。
map方法一般不需要传入参数,在方法体重,this指向当前处理的记录,可以通过this.columnName方式调用记录中的字段。
map方法不需要返回值,在方法体中,调用内置的emit方法,以emit(key, val)的形式向mapReduce内存中传递一组键值对。键值对的格式很自由,即使在当前记录中不存在的变量,只要符合语法,同样可以正常emit传出。网上的例子里,大部分emit方法中的value值都是数字,或单变量,但其实emit传递的键值可以是复杂的json甚至某个外部方法,配合对应的reduce方法,可以实现更为复杂也更有效的功能。
※ reduce方法
reduce方法的调用时机目前还不确定,基础原理是接收两个参数,key和values。其中key是emit传出的键,values是经过整合后,集中在同一个数组中的相同key的值,即使只有一个值,values也是数组形式存储的。
在此过程之前,相同key的相同value并不会被合并,map reduce概念说明中提到的词频统计的实例,是在map reduce过程完成后才产生了被合并的结果,而在reduce方法真正执行之前,相同key的相同value应该会保持emit时的状态。
在reduce方法中,遍历values数组,根据emit方法中value对应的结构,对有共同特性的结果进行统一处理,最后返回结果,在mapReduce方法的返回值中,当前key对应的值就是reduce方法返回的结果。可以总结为mapReduce方法的返回结果就是 emit中的key: reduce中的返回值 形式的json格式键值对组。
在mongodb自己的js接口中,mapReduce方法的直接返回值是方法的结果统计:
> rec = db.DATA.mapReduce(m, r, {query:{time:{$gte:'1450832129000'}}, sort: {id:1}, out:"result"})
{
"result" : "result",
"timeMillis" : 50948,
"counts" : {
"input" : 490672,
"emit" : 490672,
"reduce" : 4931,
"output" : 26
},
"ok" : 1
}
>
使用这种接口,如果需要查看详细的计算结果就需要设置mapReduce方法的out参数。
out参数的值是字符串时,返回的结果中result属性就是该字符串的值,如果只需要查看统计结果,这样也就够了。
如果需要缓存计算结果,就需要将out参数的值设置为{<option>:"临时表名"}的json结构。mapReduce的计算结果会存在mongodb的临时表中,可以通过find方法查看。
在out参数值的json结构中,<option>定义的是对临时表中相同的key的处理方式,一共有三种:
replace 使用当前reduce结果替换临时表中存在的结果
merge 当前reduce结果的key在临时表中不存在时,将reduce结果存入临时表,如果存在相同key,直接使用当前key的值替换临时表中相同key的值
reduce 如果当前key在临时表中存在,则对当前结果和已存在结果进行map reduce,重新调用map方法与reduce方法,将结果整合。
这里需要注意的是,quey结果集较大时,可能出现对同一个key多次reduce的情况,这时需要设置out的整合方法为reduce,否则临时表中存储的将只是其中一次reduce的结果,而非所有记录的reduce处理结果。在这种情况下,最好保持reduce方法的返回值结构与map方法中emit的value结构相同,使后续mapReduce可以正常执行相同的map方法与reduce方法。
这次使用的并不是mongodb自己的js接口,而是mongoose,作为第三方mongodb插件,使用起来比原生的要相对方便一些。
mongoose的model.mapReduce方法接收两个参数:mapReduce结构设定对象、回调函数(因为是异步方法)
在mapReduce对象中,mapReduceObj.map指定map方法,mapReduceObj.reduce指定reduce方法,对于out参数,mongoose的mapReduce方法默认设置为{inline: 1},以js的json对象格式返回计算结果,默认对相同key的计算结果执行reduce方法,如果预期结果规模不大,就不需要像原生方法那样再设置临时表。
在map方法和reduce方法中如果需要使用外部的js变量,可以设置mapReduceObj.scope({key:外部变量,...}),在map或reduce方法中,直接使用定义好的key,就可以取到外部变量的值。
※ mapReduce的优化
对于处理大量数据的方法,在query后将结果集排序,尽量保证在执行map方法时相同的key连续出现,减少reduce的次数,可以显著提升mapReduce的效率。这里要注意,由于调用了sort,最好将key值涉及的mongodb字段加上联合或独立索引,避免mongodb引擎使用默认sort方法产生错误。
还可以通过外部变量,过滤emit传出的结果,只保留需要的数据,减少冗余计算。
如果使用的语言支持,还可以考虑多线程方式执行,mapReduce方法的limit参数与skip参数,可以很方便的将结果集分块。
另外,最近看蝴蝶书,里边对于递归的解释让我对mapReduce方法的实现有了些感想,mapReduce中,reduce方法使用了类似递归的概念,从结果看输入,就是将输入的数据集合根据键值的不同,分成若干块,对每一块再分别调用reduce方法,逐层递归,直到每一块的内容都是一条单独的记录,无法继续分割为止。相同的map&reduce方法的递归调用。