太久沒動這里,目前人生處於一個新的開始。這次博客的內容很久前就想更新上來,但是一直沒找到合適的時間點(哈哈,其實就是懶),主要內容集中在使用Mongodb時的一些隱蔽的MapReduce問題:
1、Reduce時的計數問題
2、Reduce時的提取數據問題
另外,補充一個小tips:mongoDB中建立的索引,優先使用固定的,而不要使用范圍。
一、MapReduce時的計數問題
這個問題主要出現在使用“+1”的思路去計算累計次數時。如果在Map后的某一類中,記錄量過大,就會導致計數失敗。
具體演示如下:
原始數據(有400條一樣的存在數據庫results表中):{ "grade" : 1, "name" : "lekko", "score" : 95 }
進行MapReduce:
1 db.runCommand({ mapreduce: "results", 2 map : function Map() { 3 emit( 4 {grade:this.grade}, 5 {recnum:1,score:this.score} 6 ); 7 }, 8 reduce : function Reduce(key, values) { 9 var reduced = {recnum:0,score:0}; 10 values.forEach(function(val){ 11 reduced.score += val.score; 12 ++reduced.recnum; 13 }); 14 return reduced; 15 }, 16 finalize : function Finalize(key, reduced) { 17 return reduced; 18 }, 19 out : { inline : 1 } 20 });
滿懷希望地以為value.recnum會輸出400,結果卻是101!而value.scorce卻是輸出的正確的:38000(95*400)。本人在這疑惑了好久,並且通過更改reduce函數: function Reduce(key, values) { return {test:values}; } ,發現數據是這樣的:
在原本Reduce函數中的forEach只遍歷了第一層的數據,即101個,所以++操作也只做了101次!
經過思考,導致問題的原因關鍵就在於MapReduce中emit后的Bosn的數據格式,一個大於100的Array,會被拆分存儲,變成了非線性的鏈表結構,如圖:
那么,分數相加卻能正確,可以大膽地推測:“reduced.score += val.score;” 語句可以智能地找到所有子結點的score並相加!
最后,這里給出計數的替代方案,修改Reduce的++,改用+=操作:
1 function Reduce(key, values) { ; 2 var reduced = {recnum:0,score:0}; 3 values.forEach(function(val){ 4 reduced.score += val.score; 5 reduced.recnum += val.recnum; 6 }); 7 return reduced; 8 }
二、在Reduce中把數據提取出來組成Array
這個問題產生的原因與上面的相似,也是由於emit后的數據在reduce時是非線性的(有層次關系),所以提取數據字段時也會產生問題,為了測試,往上面所說的表中再插入3條數據:
{ "grade" : 1, "name" : "monkey", "score" : 95 }, { "grade" : 2, "name" : "sudan", "score" : 95 }, { "grade" : 2, "name" : "xiaoyan", "score" : 95 }
編寫提取出各個grade的所有人名(不重復)列表:
1 db.runCommand({ mapreduce: "results", 2 map : function Map() { 3 emit( 4 {grade:this.grade}, 5 {name:this.name} 6 ); 7 }, 8 reduce : function Reduce(key, values) { 9 var reduced = {names:[]}; 10 values.forEach(function(val) { 11 var isExist = false; 12 for(var i = 0; i<reduced.names.length; i++) { 13 var cur = reduced.names[i]; 14 if(cur==val.name){ 15 isExist = true; 16 break; 17 } 18 } 19 if(!isExist) 20 reduced.names.push(val.name); 21 }); 22 return reduced; 23 }, 24 finalize : function Finalize(key, reduced) { 25 return reduced; 26 }, 27 out : { inline : 1 } 28 });
返回結果為:
1 { "_id" : {"grade" : 1}, 2 "value" :{ "names" : [null,"lekko"]} 3 }, 4 { "_id" : {"grade" : 2}, 5 "value" :{ "names" : ["xiaoyan","sudan"]} 6 }
新插入的grade=2的兩條數據正常了,但grade=1的monkey卻不見了!采用問題一的思維方式,肯定也是在Reduce時遍歷到一個數組對象,其name值為空,也給添加進來了,monkey對象根本就沒有訪問到。
解決這一問題的方法是,拋棄MapReduce,改用Group:
1 db.results.group({ 2 key : {"grade":true}, 3 initial : {names:[]}, 4 reduce : function Reduce(val, out) { 5 var isExist = false; 6 for(var i = 0; i<out.names.length; i++) { 7 var cur = out.names[i]; 8 if(cur==val.name){ 9 isExist = true; 10 break; 11 } 12 } 13 if(!isExist) 14 out.names.push(val.name); 15 }, 16 finalize : function Finalize(out) { 17 return out; 18 }});
這樣,便可正常取到grade=1時的name非重復集合!雖說MapReduce比Group要強大,速度也要快很多,但像這種要從大量項(超過100條)中提取數據,就有很大風險了。所以,使用MapReduce時,盡量只用到累加、累減、累乘等基本操作,不要去用++、push、delete等可能會產生風險的操作!
三、補充幾個小Tips
1、使用Group或MapReduce時,如果一個分類只有一個元素,那么Reduce函數將不會執行,但Finalize函數還是會執行的。這時你要在Finalize函數中考慮一個元素與多個元素返回結果的一致性(比如,你把問題二中插入一個grade=3的數據看看,執行返回的grade=3時還有names集合嗎?)。
2、查找范圍時的索引效率,如果查詢的是一個值的范圍,它索引的優先級是很低的。比如一個表test,有海量元素,字段有'committime'、'author',建立了兩個索引:author_1、committime:-1,author:1,下面的測試證明了效率:
db.test.find({'committime':{'$gt':910713600000,'$lte':1410192000000},'author':'lekko'}).hint({committime:-1,author:1}).explain() "millis" : 49163
db.test.find({'committime':{'$gt':910713600000,'$lte':1410192000000},'author':'lekko'}).explain() author_1 "millis" : 2641