mongodb学习记录之五:mapreduce

MapReduce需要几个步骤,最开始时映射(map),将操作映射到集合中的每个文档,这个文档要么“无作为”,要么“产生一些键和x个值”。然后就是中间环节,乘坐洗牌(shuffle),按照键分组,并将产生的兼职组成列表放到对应的键中。化简(reduce)则把列表中的值化简为一个单值。这个值被返回,然后接着进行洗牌,知道每个键的列表只有一个值为止。这个值也就是最后的结果。

先推荐三篇文章,是关于mapreduce原理的解析,在我的上一篇博客中已做分享,且其中比较重要的地方已做了标注。

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

在做实例之前先准备数据,和我学习group使用的数据是一样的。

{
    "_id" : ObjectId("5343a44474d0946a30cd26b1"),
    "name" : "赵小强",
    "sex" : "男",
    "age" : 39,
    "date" : "2010-9-14",
    "salary" : 8000,
    "dep" : "测试部"
}

实例1、按部门统计每个部门的平均薪水

这是学习group时使用的第一个例子,现在使用mapreduce做一下。

db.runCommand({
    mapreduce:"emp",
    map:function(){
        emit({dep:this.dep},{salary:this.salary,count:1});
        },
    reduce:function(key,values){
        var totalS = 0;
        var totalP = 0;
        values.forEach(function(val){
            totalS += val.salary;
            totalP += val.count;
            });
        return {salary:totalS,count:totalP}
        },
    finalize:function(key, reduced){
        printjsononeline(reduced);
        var result = reduced;
        result.avgSalary = result.salary/result.count;
        delete result.salary;
        delete result.count;
        return result;
        },
    out:{inline:1}
    });

/*输出结果*/
{
    "results" : [ 
        {
            "_id" : {
                "dep" : "人力资源部"
            },
            "value" : {
                "avgSalary" : 7030.30303030303
            }
        }, 
        {
            "_id" : {
                "dep" : "信息管理部"
            },
            "value" : {
                "avgSalary" : 7962.962962962963
            }
        }, 
        {
            "_id" : {
                "dep" : "工程实施部"
            },
            "value" : {
                "avgSalary" : 7611.111111111111
            }
        }, 
        {
            "_id" : {
                "dep" : "测试部"
            },
            "value" : {
                "avgSalary" : 6476.190476190476
            }
        }, 
        {
            "_id" : {
                "dep" : "软件一部"
            },
            "value" : {
                "avgSalary" : 6078.947368421053
            }
        }, 
        {
            "_id" : {
                "dep" : "软件二部"
            },
            "value" : {
                "avgSalary" : 6939.393939393939
            }
        }, 
        {
            "_id" : {
                "dep" : "运维部"
            },
            "value" : {
                "avgSalary" : 7290.322580645161
            }
        }
    ],
    "timeMillis" : 11,
    "counts" : {
        "input" : 240,
        "emit" : 240,
        "reduce" : 21,
        "output" : 7
    },
    "ok" : 1
}
					

对于一个mapreduce,三个参数必不可少,mapreduce指定集合名,map映射函数,reduce化简函数。

map函数调用emit(key,value),第一个参数指定分组所参照的键,类似于group的key,第二个参数value指定要分组的值。

在本例中,要分组的键就是部门名称dep,要求部门的平均薪水,那么value中必不可少的必有一个salary,还有一个count:1是用来计数的。

map函数执行完后,就得到了以dep为键,多个salary为值的集合。型如:

{	key:{  "dep" : "人力资源部" },
	values:[ 	
		{ 	"salary" : 6000, 	"count" : 1 }, 	
		{ 	"salary" : 7000, 	"count" : 1 }, 	
		{ 	"salary" : 7000, 	"count" : 1 }, 	
		{ 	"salary" : 9000, 	"count" : 1 }, 	
		{ 	"salary" : 4000, 	"count" : 1 }, 	
		{ 	"salary" : 7000, 	"count" : 1 }, 	
		{ 	"salary" : 15000, 	"count" : 1 }, 	
		{ 	"salary" : 3000, 	"count" : 1 }, 	
		{ 	"salary" : 4000, 	"count" : 1 }, 	
		{ 	"salary" : 6000, 	"count" : 1 }, 	
		{ 	"salary" : 15000, 	"count" : 1 }, 	
		{ 	"salary" : 8000, 	"count" : 1 }, 	
		{ 	"salary" : 3000, 	"count" : 1 }, 	
		{ 	"salary" : 6000, 	"count" : 1 } 
	]
}
由于数据量有点大,上面只展示一条数据。你在自己做练习的时候,可以在reduce函数中使用print函数将key,values打印log日志里,从日志里查看map函数结束后的key和values。

reduce:function(key,values){
        printjsononeline(key);
        printjsononeline(values);
        var totalS = 0;
        var totalP = 0;
        values.forEach(function(val){
            totalS += val.salary;
            totalP += val.count;
            });
        return {salary:totalS,count:totalP}
        },				

从上面可以看出,values其实就是对应着key的value的数组,数组的长度就是key对应value的个数。例如上面例子中,values的长度就是每个部门中,所有salary的个数,即每个部门中的人数(如果没有人是不拿工资白干活的话)。

OK,现在可能有的人会问,既然values.length就是部门中的人数,那么为什么在reduce化简计算部门中人数总和的时候,不直接使用values.length,而要在value中加一个{count:1}来单独计数呢?

这个问题困扰我很长时间,我试过使用values.length当作部门中总人数,最后得到的结果,很显然,不正确。

我一直以为,map在映射的时候,产生的数组有问题,其实不是,原因是reduce函数的执行过程.我们看一下官方文档中的说明:

  • The reduce function should not access the database, even to perform read operations.
  • The reduce function should not affect the outside system.
  • MongoDB will not call the reduce function for a key that has only a single value. The values argument is an array whose elements are the value objects that are “mapped” to the key.
  • MongoDB can invoke the reduce function more than once for the same key. In this case, the previous output from the reduce function for that key will become one of the input values to the next reduce function invocation for that key.
  • The reduce function can access the variables defined in the scope parameter.
其中第四条,我们可以看出,对于同一个键来说,reduce函数可能执行一次或多次,如果执行多次,那么上一次reduce的输出将作为下一次reduce的输入继续执行。对于一个key来说,reduce到底执行多少次,我们并不知道。换句话说,我们并不知道mongodb是如何执行reduce的,我们不知道哪个key先reduce,哪个key后reduce,我们也不知道每个key到底执行多少次reduce,最优选择则是mongodb自己决定的。



图片来源:http://www.mongovue.com/2010/11/03/yet-another-mongodb-map-reduce-tutorial/

从图片中我们可以看出,第一次reduce后的输出r1将作为第二次reduce的输入继续执行reduce.

具体原理在官方文档中说明:

直接对ABC进行reduce,和先对AB进行reduce,然后将输出再和C进行reduce是等效的。

OK,我们看一下我们的例子中是如何进行reduce的

这里我们只拿出一个key对应的values进行分析。

/*第一次reduce*/
[
  { 	"salary" : 6000, 	"count" : 1 }, 
  { 	"salary" : 7000, 	"count" : 1 }, 	
  { 	"salary" : 7000, 	"count" : 1 }, 	
  { 	"salary" : 9000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 }, 	
  { 	"salary" : 7000, 	"count" : 1 }, 	
  { 	"salary" : 15000, 	"count" : 1 }, 	
  { 	"salary" : 3000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 }, 	
  { 	"salary" : 6000, 	"count" : 1 }, 	
  { 	"salary" : 15000, 	"count" : 1 }, 	
  { 	"salary" : 8000, 	"count" : 1 }, 	
  { 	"salary" : 3000, 	"count" : 1 }, 	
  { 	"salary" : 6000, 	"count" : 1 } 
]
/*第二次reduce*/
[
  { 	"salary" : 100000, 	"count" : 14 }, 	
  { 	"salary" : 3000, 	"count" : 1 }, 	
  { 	"salary" : 6000, 	"count" : 1 }, 	
  { 	"salary" : 10000, 	"count" : 1 }, 	
  { 	"salary" : 6000, 	"count" : 1 }, 	
  { 	"salary" : 9000, 	"count" : 1 }, 	
  { 	"salary" : 3000, 	"count" : 1 }, 	
  { 	"salary" : 15000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 }, 	
  { 	"salary" : 7000, 	"count" : 1 }, 	
  { 	"salary" : 9000, 	"count" : 1 }, 	
  { 	"salary" : 10000, 	"count" : 1 }, 	
  { 	"salary" : 3000, 	"count" : 1 }, 	
  { 	"salary" : 5000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 } 
]
/*第三次reduce*/
[
  { 	"salary" : 194000, 	"count" : 28 }, 	
  { 	"salary" : 9000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 }, 	
  { 	"salary" : 15000, 	"count" : 1 }, 	
  { 	"salary" : 6000, 	"count" : 1 }, 	
  { 	"salary" : 4000, 	"count" : 1 } 
]

在本例中,一个键执行了三次reduce,总共7个键,因此总共执行了21次reduce,正好对应着上面的、结果中counts.reduce=21.

到这就能看出,为什么不能使用values.length来计算部门中的总人数了,因为reduce不仅仅执行一次。而单独使用{count:1}则可以,count可以累加,而values.length则不行。

OK,reduce的基本原理到这里就差不多基本搞明白了,最关键的就是将每个函数的输入和输出搞清楚。使用print函数将数据打印到log日志里,从日志里逐个追踪分析,便能轻而易举的搞清楚其原理。

finalize函数是可选的,是最后的处理和过滤函数。注意两个参数,key还是和上面的key一样,reduced是reduce函数的返回值,上例中,我们在reduce里只拿到了薪水总和和人数总和,在finalize函数里我们便可使用两者相除得到平均薪水。

其余的可选参数,可以慢慢做小实验进行理解。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章