Mongodb聚合(三) 3. MapReduce Mapreduce很是強大與靈活,Mongodb使用javascript做爲查詢語言,能夠表示任意複雜的邏輯。 Mapreduce很是慢,不該該用在實際的數據分析中。 Mapreduce能夠在多臺服務器之間並行執行,能夠將一個問題拆分爲多個小問題,以後將各個小問題發送到不一樣的機器上,每臺機器只負責完成一部分工做,全部的機器完成時,將這些零碎的解決方案合併爲一個完整的解決方案。 最開始的是映射(map),將操做映射到集合中的各個文檔,而後是中間環節,成爲洗牌(shuffle),按照鍵分組,將產生的鍵值組成列表放在對應的鍵中。化簡(reduce)則是把列表中的值化簡爲一個單值。 3.1 找出集合中的全部鍵 MongoDB假設你的模式是動態的,因此並不會跟蹤記錄每一個文檔的鍵。一般找到集合中全部文檔的全部鍵的最好方式就是MapReduce。 在映射環節,map函數使用特別的emit函數返回要處理的值。emit會給MapReduce一個鍵和一個值。 這裏用emit將文檔某個鍵的計數返回。this就是當前映射文檔的引用: map = function() { emit(this.country, {count : 1}); } reduce接受兩個參數,一個是key,就是emit返回的第一個值,還有一個數組,由一個或多個鍵對應的{count : 1}文檔組成。 reduce = function(key, value) { var result = {count : 0}; for (var i = 0; i < value.length; i++) { result.count += value[i].count; } return result; } 示例表數據: { "_id" : 38, "country" : "japan", "money" : 724 } { "_id" : 39, "country" : "germany", "money" : 520 } { "_id" : 40, "country" : "india", "money" : 934 } { "_id" : 41, "country" : "china", "money" : 721 } { "_id" : 42, "country" : "germany", "money" : 156 } { "_id" : 43, "country" : "canada", "money" : 950 } { "_id" : 44, "country" : "india", "money" : 406 } { "_id" : 45, "country" : "japan", "money" : 776 } { "_id" : 46, "country" : "canada", "money" : 468 } { "_id" : 47, "country" : "germany", "money" : 262 } { "_id" : 48, "country" : "germany", "money" : 126 } { "_id" : 49, "country" : "japan", "money" : 86 } { "_id" : 50, "country" : "canada", "money" : 870 } { "_id" : 51, "country" : "india", "money" : 98 } { "_id" : 52, "country" : "india", "money" : 673 } { "_id" : 53, "country" : "japan", "money" : 487 } { "_id" : 54, "country" : "india", "money" : 681 } { "_id" : 55, "country" : "canada", "money" : 491 } { "_id" : 56, "country" : "japan", "money" : 98 } { "_id" : 57, "country" : "china", "money" : 172 } Type "it" for more 運行結果: db.foo.mapReduce(map, reduce, {out : "collection"}) { "result" : "collcetion", "timeMillis" : 83, "counts" : { "input" : 99, "emit" : 99, "reduce" : 5, "output" : 5 }, "ok" : 1, "$gleStats" : { "lastOpTime" : Timestamp(1399168165, 15), "electionId" : ObjectId("535a2ce15918f42de9ab1427") }, } result:存放的集合名 timeMillis:操做花費的時間,單位是毫秒 input:傳入文檔數目 emit:此函數被調用的次數 reduce:此函數被調用的次數 output:最後返回文檔的個數 查看下collection結果內容: db.collection.find(); { "_id" : "canada", "value" : { "count" : 19 } } { "_id" : "china", "value" : { "count" : 15 } } { "_id" : "germany", "value" : { "count" : 25 } } { "_id" : "india", "value" : { "count" : 20 } } { "_id" : "japan", "value" : { "count" : 20 } } 3.2 MapRecude其餘的鍵 "finalize" : function 能夠將reduce的結果發送給這個鍵,這是整個處理過程的最後一步。 "keeptemp自動爲true。" : boolean 若是爲true,則在鏈接關閉後結果保存,不然不保存。 "out" : string 輸出集合的名稱,若是設置,keeptemp自動爲true。 "query" : document 在發往map前,先用指定條件過濾文檔。 "sort" : document 在發往map前,先進行排序。 "limit" : integer 發往map函數的文檔數量上限。 "scope" : document 能夠在javascripts代碼中使用的變量。 "verbose" : boolean 是否記錄詳細的服務器日誌。 3.2.1 finalize函數 可使用finalize函數做爲參數,會在最後一個reduce輸出結果後執行,而後將結果保存在臨時集合裏。 3.2.2 保存結果集合 默認狀況下,執行mapreduce時建立一個臨時集合,集合名稱爲mr.stuff.ts.id,即mapreduce.集合名.時間戳.數據庫做業ID。MongoDB會在調用的鏈接關閉時自動銷燬這個集合。 3.2.3 對子文檔執行mapreduce 每一個傳遞給map的文檔都須要先反序列化,從BSON對象轉換爲js對象,這個過程很是耗時,能夠先對文檔過濾來提升map速度,能夠經過"query","limit"和"sort"等對文檔進行過濾。 "query"的值是一個查詢文檔。 "limit","sort"配合能夠發揮很大的做用。 "query","limit"和"sort"能夠隨意組合使用。 3.2.4 做用域 做用域鍵"scope",能夠用變量名:值這樣普通的文檔來設置該選項, 3.2.5 獲取更多的輸出 設置verbose爲true,能夠將mapreduce過程更多的信息輸出到服務器日誌上。 4 聚合命名 count和distinct操做能夠簡化爲普通命令,不須要使用聚合框架。 4.1 count count返回集合中的文檔數量: db.foo.count() => 99 能夠傳入一個查詢文檔: db.foo.count({country : "china"}) => 15 增長查詢條件會使count變慢。 4.2 distinct distinct用來找出給定鍵的全部不一樣值。使用時必須指定集合和鍵。 db.runCommand({ "distinct" : "foo", "key" : "country"}) => { "values" : [ "japan", "germany", "india", "china", "canada" ], "stats" : { "n" : 99, "nscanned" : 99, "nscannedObjects" : 99, "timems" : 22, "cursor" : "BasicCursor" }, "ok" : 1, "$gleStats" : { "lastOpTime" : Timestamp(1399171995, 15), "electionId" : ObjectId("535a2ce15918f42de9ab1427") } } 4.3 group 使用group能夠進行更爲複雜的聚合。先選定分組所依據的鍵,而後根據選定鍵的不一樣值分爲若干組,而後對每個分組進行聚合,獲得結果文檔。 插入示例數據: var name = ["Caoqing", "Spider-man", "Garfield"] for (var i = 0; i < 10000; i++) { iname = name[Math.floor(Math.random() * name.length)]; date = new Date().getTime(); number = Math.floor(100 * Math.random()); db.coll.insert({_id : i, name : iname, time : date, age : number}); } 生成的列表中包含最新的時間和最新的時間對應的年紀。 能夠安裝name進行分組,而後取出每一個分組中date最新的文檔,將其加入結果集。 db.runCommand({"group" : { "ns" : "coll", "key" : {"name" : true}, "initial" : {"time" : 0}, "$reduce" : function(doc, prev) { if (doc.time > prev.time) { prev.age = doc.age; prev.time = doc.time; } } }}) "ns" : "coll" 指定進行分組的集合。 "key" : {"name" : true} 指定分組依據的鍵。 "initial" : {"time" : 0} 初始化time值,做爲初始Wednesday傳遞給後續過程。每組成員都會使用這個累加器。 結果: "$reduce" : function(doc, prev) {...} { "retval" : [ { "name" : "Spider-man", "time" : 1399179398567, "age" : 55 }, { "name" : "Garfield", "time" : 1399179398565, "age" : 85 }, { "name" : "Caoqing", "time" : 1399179398566, "age" : 86 } ], "count" : 10000, "keys" : 3, "ok" : 1, "$gleStats" : { "lastOpTime" : Timestamp(1399179362, 1), "electionId" : ObjectId("535a2ce15918f42de9ab1427") } } 若是有文檔不存在指定分組的鍵,這些文檔會單獨分爲一組,缺失的鍵會使用name:null這樣的形式。以下: db.coll.insert({age : 5, time : new Date().getTime()}) 返回結果: ... { "name" : null, "time" : 1399180685288, "age" : 5 } "count" : 10001, "keys" : 4, ... 爲了排除不包含指定用於分組的鍵的文檔,能夠在"condition"中加入"name":{"$exists" : true}。 db.runCommand({"group" : { "ns" : "coll", "key" : {"name" : true}, "initial" : {"time" : 0}, "$reduce" : function(doc, prev) { if (doc.time > prev.time) { prev.age = doc.age; prev.time = doc.time; } }, "condition" : {"name" : {"$exists" : true}} }}) 4.3.1 使用完成器 完成器(finalizer)用於精簡從數據庫傳到用戶的數據,由於group命令的輸出結果須要可以經過單次數據庫響應返回給用戶。 4.3.2 將函數做爲鍵使用 分組條件能夠很是複雜,不是單個鍵,例如分組時按照類別分組dog和DOG是兩個徹底不一樣的組,爲了消除大小寫差別,能夠定義一個函數決定文檔分組所依據的鍵。 定義分組函數須要用到"$keyf"鍵, db.foo.group({ "ns" : "foo", "$keyf" : function(x) { return x.category.toLowerCase(); }; "initial" : ..., ...... })