Date: 2016-03-09
Summary: 在實(shí)際應(yīng)用中,用MongoDB做了數(shù)據(jù)庫(kù)查詢(xún)之后返回的數(shù)據(jù)量很大琳拨,想要做一些比較復(fù)雜的統(tǒng)計(jì)和聚合操作做花費(fèi)的時(shí)間超過(guò)了數(shù)據(jù)庫(kù)操作耗時(shí),嘗試用MongoDB中的MapReduce機(jī)制進(jìn)行了實(shí)現(xiàn)缚陷,速度提升還是很明顯的祥国!
使用MongoDB的MapReduce進(jìn)行查詢(xún)和數(shù)據(jù)組織
MapReduce是MongoDB中最復(fù)雜的查詢(xún)機(jī)制之一。它通過(guò)兩個(gè)JavaScript函數(shù)實(shí)現(xiàn)查詢(xún):map和reduce碳抄。這兩個(gè)函數(shù)完全由用戶(hù)自定義愉老,并且能夠提供及其強(qiáng)大的靈活性!實(shí)際應(yīng)用中遇到的問(wèn)題無(wú)法直接使用MongoDB的聚合框架實(shí)現(xiàn)剖效,才考慮到使用MapReduce嫉入。下面將通過(guò)一個(gè)簡(jiǎn)短的例子演示如何使用MapReduce焰盗。該例子的原型來(lái)自于實(shí)際的問(wèn)題,由于數(shù)據(jù)的保密性咒林,我采用MongoDB官方對(duì)外發(fā)布的Zip Code數(shù)據(jù)集進(jìn)行舉例熬拒。
問(wèn)題描述
目前的流程是這樣的,首先按照一定的條件對(duì)collection進(jìn)行查詢(xún)垫竞,在查詢(xún)結(jié)果中想要統(tǒng)計(jì)每一個(gè)key的loc屬性(地理經(jīng)緯度)澎粟,并把所有的loc加入到一個(gè)數(shù)組,然后返回欢瞪。剛開(kāi)始統(tǒng)計(jì)的部分是在程序中進(jìn)行的活烙,由于數(shù)據(jù)量很大,查詢(xún)的部分并不慢遣鼓,主要是統(tǒng)計(jì)的部分形成了瓶頸啸盏。想到用MongoDB的聚合框架來(lái)解決問(wèn)題,研究了一下骑祟,發(fā)現(xiàn)功能有限回懦。看到了MapReduce非常靈活次企,感覺(jué)肯定可以解決粉怕,所以決定嘗試一下,最后比較一下性能有多大的提升抒巢。
數(shù)據(jù)集導(dǎo)入
首先下載zips.json文件贫贝,放到當(dāng)前目錄下,然后使用mongoimport工具進(jìn)行導(dǎo)入:
$mongoimport -d test -c zipcodes < zips.json
然后看下是否已經(jīng)導(dǎo)入成功了:
use test
db.zipcodes.find()
有數(shù)據(jù)則導(dǎo)入成功蛉谜!
{ "_id" : "01002", "city" : "CUSHMAN", "loc" : [ -72.51565, 42.377017 ], "pop" : 36963, "state" : "MA" }
{ "_id" : "01001", "city" : "AGAWAM", "loc" : [ -72.622739, 42.070206 ], "pop" : 15338, "state" : "MA" }
{ "_id" : "01005", "city" : "BARRE", "loc" : [ -72.108354, 42.409698 ], "pop" : 4546, "state" : "MA" }
{ "_id" : "01007", "city" : "BELCHERTOWN", "loc" : [ -72.410953, 42.275103 ], "pop" : 10579, "state" : "MA" }
{ "_id" : "01008", "city" : "BLANDFORD", "loc" : [ -72.936114, 42.182949 ], "pop" : 1240, "state" : "MA" }
下面我們就以state為key稚晚,來(lái)返回collection中全部loc的數(shù)組。
解決方法1:使用傳遞字符串的方法
根據(jù)參考3頁(yè)面的問(wèn)題回復(fù)來(lái)看型诚,"Currently, the return value from a reduce function cannot be an array (it's typically an object or a number)." ****目前reduce函數(shù)的返回值還不能是數(shù)組客燕!可以是數(shù)字和對(duì)象!如果直接返回?cái)?shù)組會(huì)報(bào)錯(cuò):"reduce -> multiple not supported yet"狰贯,參考3中介紹了這個(gè)錯(cuò)誤的解決方法也搓。下面的一位大神提出了一個(gè)最簡(jiǎn)單的方法:把返回值轉(zhuǎn)換成一個(gè)字符串返回不就行了。js中的字符串也是對(duì)象啊涵紊。這樣的話(huà)傍妒,在外面再對(duì)字符串進(jìn)行解析,也是一個(gè)不錯(cuò)的方法摸柄。那么就先來(lái)試一下:
//solution 1 使用傳字符串的方法
var map = function() {
emit(this.state, this.loc);
};
var reduce = function(key, values) {
//print(values);
return values.toString();
};
db.zipcodes.mapReduce(
map,
reduce,
{
query:{state:"NY"},
out:{inline:1}
}
)
在map函數(shù)中把state作為key颤练,loc作為value發(fā)送到reduce函數(shù)。reduce函數(shù)中把接收到的每個(gè)key的匯總值直接轉(zhuǎn)成字符串返回驱负。在控制臺(tái)中的運(yùn)行結(jié)果如下:
{
"results" : [
{
"_id" : "NY",
"value" : "-72.017834,41.263934,-73.996705,40.74838,-73.987681,40.715231,
.......
-73.981328,40.737476,-73.99963,40.740225,-78.242958,43.332563"
}
],
"timeMillis" : 121,
"counts" : {
"input" : 1595,
"emit" : 1595,
"reduce" : 16,
"output" : 1
},
"ok" : 1
}
數(shù)據(jù)太長(zhǎng)了只能省略了中間的部分嗦玖,從結(jié)果來(lái)看這種方法是可行的患雇!
對(duì)了,代碼中注釋掉的print那一行是用來(lái)調(diào)試mapreduce的輸出信息的宇挫,它會(huì)把結(jié)果輸出到mongodb的日志文件中苛吱,打開(kāi)日志文件就能看到,這樣很方便器瘪,更容易理解這個(gè)過(guò)程翠储。具體可以參考4,5娱局,6進(jìn)行了解和學(xué)習(xí)彰亥。
解決方法2:使用傳遞對(duì)象的方法
下面使用傳遞對(duì)象的方法來(lái)返回我們需要的數(shù)組咧七,簡(jiǎn)單起見(jiàn)衰齐,我們先把每個(gè)state的pop裝到一個(gè)數(shù)組里。具體實(shí)現(xiàn)代碼如下:
//solution 2 : population example 傳對(duì)象的方法傳pop數(shù)組
var map = function() {
emit(this.state, {p:[this.pop]});
};
var reduce = function(key, values) {
var ret = {p:[]};
for(var i = 0; i < values.length; i++){
ret.p.push(values[i].p[0]);
}
return ret;
};
db.zipcodes.mapReduce(
map,
reduce,
{
query:{state:"NY"},
out:"NYpop"
}
)
這里的有幾點(diǎn)是需要注意的继阻,最重要的一點(diǎn)就是:The value from the map function needs to have the same shape as the return from reduce. 所以map函數(shù)中value是什么樣耻涛,reduce函數(shù)中的返回值就要是什么樣!不然會(huì)有很多問(wèn)題瘟檩,返回是null或者undefined都是很常見(jiàn)的抹缕。還有一點(diǎn)就是輸出模式out和上面的例子不一樣,這種是輸出成了一個(gè)collection墨辛,上面是直接輸出在控制臺(tái)了卓研,自己體會(huì)一下就好。輸出結(jié)果如下:
{
"result" : "NYpop",
"timeMillis" : 178,
"counts" : {
"input" : 1595,
"emit" : 1595,
"reduce" : 16,
"output" : 1
},
"ok" : 1
}
好的睹簇,下面實(shí)現(xiàn)最后一步奏赘,就要達(dá)到目標(biāo)了。我們要把每個(gè)state的loc裝到一個(gè)數(shù)組里太惠,loc本身由經(jīng)緯度兩個(gè)字段組成磨淌,也是一個(gè)數(shù)組,也就是說(shuō)凿渊,返回的結(jié)果是數(shù)組的數(shù)組梁只。具體mapreduce函數(shù)如下:
//solution 3 統(tǒng)計(jì)loc
var map = function() {
emit(this.state, {locs:[this.loc]});
};
var reduce = function(key, values) {
var ret = {locs:[]}
for(var i = 0; i < values.length; i++){
ret.locs.push(values[i].locs[0]);
}
return ret;
};
db.zipcodes.mapReduce(
map,
reduce,
{
query:{state:"NY"},
out:{inline:1}
}
)
輸出結(jié)果如下:
{
"results" : [
{
"_id" : "NEW YORK",
"value" : {
"locs" : [
[
-73.996705,
40.74838
],
[
-73.987681,
40.715231
],
......
[
-74.016323,
40.710537
]
]
}
}
],
"timeMillis" : 77,
"counts" : {
"input" : 40,
"emit" : 40,
"reduce" : 1,
"output" : 1
},
"ok" : 1
}
整個(gè)流程走通還是不太容易的,遇到了很多問(wèn)題埃脏,其中一個(gè)就是嵌套數(shù)組的問(wèn)題搪锣,參考7中解釋了嵌套數(shù)組的形成原因,并給出了解決方法彩掐,已經(jīng)加到代碼中淤翔。
參考
- MongoDB官方文檔https://docs.mongodb.org/manual/core/map-reduce/ 解釋很詳細(xì),圖片到位佩谷,簡(jiǎn)單易懂
- http://thejackalofjavascript.com/mapreduce-in-mongodb/ MapReduce使用的例子
- http://stackoverflow.com/questions/8175015/mongodb-mapreduce-reduce-multiple-not-supported-yethttp://stackoverflow.com/questions/8175015/mongodb-mapreduce-reduce-multiple-not-supported-yet
- http://stackoverflow.com/questions/13963483/how-to-get-print-output-for-debugging-map-reduce-in-mongoid
- http://www.cnblogs.com/yuechaotian/archive/2013/02/26/2933455.html
- http://stackoverflow.com/questions/7527126/mongodb-how-to-debug-map-reduce-on-mongodb-shell
- http://stackoverflow.com/questions/8099991/rejoining-split-mapreduce-arrays-in-mongohttp://stackoverflow.com/questions/8099991/rejoining-split-mapreduce-arrays-in-mongo