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函數裏我們便可使用兩者相除得到平均薪水。

其餘的可選參數,可以慢慢做小實驗進行理解。



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