mongodb進階二之mongodb聚合
這篇說說mongodb的聚合
一:mongodb中有很多聚合框架,從而實現文件的變換和組合,主要有一下構件
構件類別 操作符
篩選(filtering) $match
投射(projecting) $project
分組(grouping $group
排序(sorting) $sort
限制(limiting) $limit
跳過(skipping) $skip
如果需要聚合資料,那麼要使用aggregate方法
db.collection.aggregate(聚合條件);
單個操作,傳入一個json物件作為集合條件,如
db.users.aggregate({
$project:{
_id:0,
name:1,
}
})
如果需要多個操作符,傳入一個數組作為條件,如
db.users.aggregate([
{ $skip : 5 },
{ $project:{ _id:0, name:1, } }
])
1.1:$match匹配
$match用於對文件集合進行篩選,之後就可以在篩選得到的文件子集上做聚合。
例如,如果想對北京(簡寫BJ)的使用者做統計,就可以使用{$match:{"area":"BJ"}}。"$match"可以使用所有常規的查詢操作符("$gt"、"$lt"、"$in"等)。有一個裡外需要注意:不能在"$match"中使用地理空間操作符。
通常,在實際使用中應該儘可能將"$match"放在管道的前面位置。這樣做有兩個好處:
一是可以快速將不需要的文件過濾掉,以減少管道的工作量;
二是如果在投射和分組之前執行"$match",查詢可以使用索引。
1.2:$project投射
相對於“普通”的查詢而言,管道中的投射操作更加強大。使用"$project"可以從子文件中提取欄位,可以重新命名欄位,還可以在這些欄位上進行一些有意思的操作。
最簡單的一個"$project"操作是從文件中選擇想要的欄位。可以指定包含或者不包含一個欄位,它的語法和查詢中的第二個引數類似。如果在原來的集合上執行下面的程式碼,返回的結果文件中只包含一個"author"欄位。
db.articles.aggregate({"$project":{"author":1,"_id":0})
預設情況下,如果文件中存在"_id"欄位,這個欄位就會被返回。
趕快親自動手敲下,看看執行結果。
也可以將投射過的欄位進行重新命名。例如,可以將每個使用者文件的"_id"在返回結果中重新命名為"userId":
db.articles.aggregate({"$project":{"userId":"$_id","_id":0}});
這裡的"$fieldname"語法是為了在聚合框架中引用fieldname欄位(上面的例子中是"_id")的值。例如,"$age"會被替換為"age"欄位的內容(可能是數值,可能是字串),"$tag.3"會被替換為tags陣列中的第4個元素。所以,上面例子中的"$_id"會被替換為進入管道的每個文件的"_id"欄位的值。
注意,必須明確指定將"_id"排除,否則這個欄位的值會被返回兩次:一次標記為"userId",一次被標記為"_id"。可以使用這種技術生成欄位的多個副本,以便在之後"$group"中使用。
繼續學習
1.3:$group分組
$group操作可以將文件依據特定欄位的不同值進行分組。舉例:
如果有一個學生集合,希望按照分數等級將學生分為多個組,可以根據"grade"欄位進行分組。
如果選定了需要進行分組的欄位,就可以將選定的欄位傳遞給"$group"函式的"_id"欄位。對於上面的例子,相應程式碼如下:
{"$group":{"_id":"$grade"}}
例如,學生分數等級進行分組的結果可能是:
{"result":[{"_id":"A+"},{"_id":"A"},{"_id":"A-"},...,{"_id":"F"}],"ok":1}
分組操作符
這些分組操作符允許對每個分組進行計算,得到相應的結果。
1.4:$unwind拆分
拆分(unwind)可以將陣列中的每一個值拆分為單獨的文件。
例如,如果有一篇擁有多條評論的部落格文章,可以使用$unwind將每條評論拆分為一個獨立的文件:
db.blog.findOne()
{
"_id":ObjectId("5359f6f6ec7452081a7873d7"),
"author":"Tom",
"conments":[
{
"author":"Mark",
"date":ISODate("2014-01-01T17:52:04.148Z"),
"text":"Nice post"
},
{
"author":"Bill",
"date":ISODate("2014-01-01T17:52:04.148Z"),
"text":"I agree"
}
]
}
db.blog.aggregate({"$unwind":"$comments"})
{
"results":
{
"_id":ObjectId("5359f6f6ec7452081a7873d7"),
"author":"Tom",
"comments":{
"author":"Mark",
"date":ISODate("2014-01-01T17:52:04.148Z"),
"text":"Nice post"
}
},
{
"_id":ObjectId("5359f6f6ec7452081a7873d7"),
"author":"Tom",
"comments":{
"author":"Bill",
"date":ISODate("2014-01-01T17:52:04.148Z"),
"text":"I agree"
}
}
}
如果希望在查詢中得到特定的子文件,這個操作符就會非常有用:先使用"$unwind"得到所有子文件,再使用"$match"得到想要的文件。例如,如果要得到特定使用者的所有評論(只需要得到評論,不需要返回評論所屬的文章),使用普通的查詢是不可能做到的。但是,通過提取、拆分、匹配、就很容易了:
db.blog.aggregate({"$project":{"coomments":"$comments"}},
{"$unwind":"$comments"},
{"$match":{"comments.author":"Mark"}})
由於最後得到的結果仍然是一個"comments"子文件,所以你可能希望再做一次投射,以便讓輸出結果更優雅。
1.5:sort排序
可以根據任何欄位(或者多個欄位)進行排序,與普通查詢中的語法相同。如果要對大量的文件進行排序,強烈建議在管道的第一階段進行排序,這時的排序操作可以使用索引。否則,排序過程就會比較慢,而且會佔用大量記憶體。
可以在排序中使用文件中實際存在的欄位,也可以使用在投射時重新命名的欄位:
db.employees.aggregate(
{
"$project":{
"compensation":{
"$add":["$salary","$bonus"]
},
name:1
}
},
{
"$sort":{"compensation":-1,"name":1}
}
)
這個例子會對員工排序,最終的結果是按照報酬從高到低,姓名從A到Z的順序排序。
排序的方向可以是1(升序)和-1(降序)。
與前面講過的"$group"一樣,"$sort"也是一個無法使用流式工作方式的操作符。"$sort"也必須要接收到所有文件之後才能進行排序。在分片環境下,先在各個分片上進行排序,然後將各個分片的排序結果傳送到mongos做進一步處理。
1.6:$limit會接受一個數字n,返回結果集中的前n個文件。
$skip也是接受一個數字n,丟棄結果集中的前n個文件,將剩餘文件作為結果返回。在"普通"查詢中,如果需要跳過大量的資料,那麼這個操作符的效率會很低。在聚合中也是如此,因為它必須要先匹配到所有需要跳過的文件,然後再將這些文件丟棄。
1.7:使用管道
應該儘量在管道的開始階段(執行"$project"、"$group"或者"$unwind"操作之前)就將盡可能多的文件和欄位過濾掉。管道如果不是直接從原先的集合中使用資料,那就無法在篩選和排序中使用索引。如果可能,聚合管道會嘗試對操作進行排序,以便能夠有效使用索引。
二:聚合命令
2.1:count
count是最簡單的聚合工具,用於返回集合中的文件數量:
db.users.count()
0
db.users.insert({"x":1})
db.users.count()
1
不論集合有多大,count都會很快返回總的文件數量。
也可以給count傳遞一個查詢文件,Mongo會計算查詢結果的數量:
db.users.insert({"x":2})
db.users.count()
2
db.users.count({"x":1})
1
對於分頁顯示來說總數非常必要:“共439個,目前顯示0~10個”。但是,增加查詢條件會使count變慢。count可以使用索引,但是索引並沒有足夠的元資料提供count使用,所以不如直接使用查詢來得快。
2.2:distinct
distinct用來找出給定鍵的所有不同值。使用時必須指定集合和鍵。
db.runCommand({"distinct":"people","key":"age"})
假設集合中有如下文件
{name:"Ada",age:20}
{name:"Fred",age:35}
{name:"Susan",age:60}
{name:"Andy",age:35}
如果對"age"鍵使用distinct,會得到所有不同的年齡:
db.runCommand({"distinct":"people","key":"age"})
{"values":[20,35,60],"ok":1}
2.3:group
使用group可以執行更復雜的聚合。先選定分組所依據的鍵,而後MongoDB就會將集合依據選定鍵的不同值分成若干組。然後可以對每一個分組內的文件進行聚合,得到一個結果文件。
如果你熟悉SQL,那麼這個group和SQL中的GROUP BY 差不多。
假設現在有個跟蹤股票價格的站點。從上午10點到下午4點每隔幾分鐘就會更新某隻股票的價格,並儲存在MongoDB中。現在報表程式要獲得近30天的收盤價。用group就可以輕鬆辦到。
股票集合中包含數以千計如下形式的文件:
{"day" : "2010/10/03","time" : "10/3/2010 03:57:01 GMT-400","price" : 4.23}
{"day" : "2010/10/04","time" : "10/4/2010 11:28:39 GMT-400","price" : 4.27}
{"day" : "2010/10/03","time" : "10/3/2010 05:00:23 GMT-400","price" : 4.10}
{"day" : "2010/10/06","time" : "10/6/2010 05:27:58 GMT-400","price" : 4.30}
{"day" : "2010/10/04","time" : "10/4/2010 08:34:50 GMT-400","price" : 4.01}
我們需要的結果列表中應該包含每天的最後交易時間和價格,就像下面這樣:
[
{"time" : "10/3/2010 05:00:23 GMT-400","price" : 4.10}
{"time" : "10/4/2010 11:28:39 GMT-400","price" : 4.27}
{"time" : "10/6/2010 05:27:58 GMT-400","price" : 4.30}
]
先把集合按照"day"欄位進行分組,然後在每個分組中查詢"time"值最大的文件,將其新增到結果集中就完成了。整個過程如下所示:
> db.runCommand({"group" : {
... "ns" : "stocks",
... "key" : "day",
... "initial" : {"time" : 0},
... "$reduce" : function(doc,prev){
... if(doc.time > prev.time){
... prev.price = doc.price;
... prev.time = doc.time;
... }
... }}})
三:MapReduce
好煩,說到這,好想跟大夥說說hadoop中的mapreduce,可是最好不要說串掉,要不然就誤人子弟了,其實原理都是一樣的啦
MapReduce是一種程式設計模型,用於大規模資料集(大於1TB)的並行運算。概念"Map(對映)"和"Reduce(歸約)",和它們的主要思想,都是從函數語言程式設計語言裡借來的,還有從向量程式語言裡借來的特性。它極大地方便了程式設計人員在不會分散式並行程式設計的情況下,將自己的程式執行在分散式系統上。 當前的軟體實現是指定一個Map(對映)函式,用來把一組鍵值對對映成一組新的鍵值對,指定併發的Reduce(歸約)函式,用來保證所有對映的鍵值對中的每一個共享相同的鍵組。
3.1:找出集合中的所有鍵
MongoDB沒有模式,所以並不知曉每個文件有多少個鍵.通常找到集合的所有鍵的做好方式是用MapReduce。 在對映階段,想得到文件中的每個鍵.map函式使用emit 返回要處理的值.emit會給MapReduce一個鍵和一個值。 這裡用emit將文件某個鍵的記數(count)返回({count:1}).我們為每個鍵單獨記數,所以為文件中的每一個鍵呼叫一次emit。 this是當前文件的引用:
> map=function(){
... for(var key in this){
... emit(key,{count:1})
... }};
這樣返回了許許多多的{count:1}文件,每一個都與集合中的一個鍵相關.這種有一個或多個{count:1}文件組成的陣列,會傳遞給reduce函式.reduce函式有兩個引數,一個是key,也就是emit返回的第一個值,另一個引數是陣列,由一個或者多個與鍵對應的{count:1}文件組成。
> reduce=function(key,emits){
... total=0;
... for(var i in emits){
... total+=emits[i].count;
... }
... return {count:total};
... }
reduce要能被反覆被呼叫,不論是對映環節還是前一個化簡環節。reduce返回的文件必須能作為reduce的第二個引數的一個元素。如x鍵對映到了3個文件{"count":1,id:1},{"count":1,id:2},{"count":1,id:3} 其中id鍵用於區別。MongoDB可能這樣呼叫reduce:
>r1=reduce("x",[{"count":1,id:1},{"count":1,id:2}])
{count:2}
>r2=reduce("x",[{"count":1,id:3}])
{count:1}
>reduce("x",[r1,r2])
{count:3}
不能認為第二個引數總是初始文件之一(比如{count:1})或者長度固定。reduce應該能處理emit文件和其他reduce返回結果的各種組合。
總之,MapReduce函式可能會是下面這樣:
> mr = db.runCommand({"mapreduce" : "foo", "map" : map,"reduce" : reduce})
{
"reduce" : "tmp.mr.mapreduce_1266787811_1", // 這是存放MapReduce結果集合名,臨時集合連線關閉自動刪除
"timeMillis" : 12, // 操作花費的時間,單位毫秒
"count" : {
"input" : 6 //發往到map函式的文件個數
"emit" : 14 //在map函式中emit被呼叫的次數
"output" : 5 //結果集合中的文件數量
},
"ok" : true
}
3.2:網頁分類
我們有這樣一個網站,使用者可以在其上提交他們喜愛的連結url,比如匯智網(http://www.hubwiz.com),並且提交者可以為這個url新增一些標籤,作為主題,其他使用者可以為這條資訊打分。我們有一個集合,收集了這些資訊,然後我們需要看看哪種主題最為熱門,熱門程度由最新打分日期和所給分數共同決定。
首先建立一個map函式,發出(emit)標籤和一個基於流行度和新舊程度的值。
> map = function(){
... for(var i in this.tags){
... var recency = 1/(new Date() - this.date);
... var score = recency * this.score;
... emit(this.tags[i], {"urls":[this.url], "score":this.score});
... }
... };
現在就化簡同一個標籤的所有值,以得到這個標籤的分數:
> reduce = function(key, emits) {
... var total = {"urls":[], "score":0};
... for(var i in emits) {
... emits[i].urls.forEach(function(url) {
... total.urls.push(url);
... });
... total.score += emits[i].score;
... }
... return total;
... };
3.2:MongoDB和MapReduce
前面兩個例子只用到了MapReduce、map和reduce鍵。這3個鍵是必需的,但是MapReduce命令還有很多可選的鍵。
"finalize" : 函式
將reduce的結果傳送給這個鍵,這是處理過程的最後一步。
"keeplize" : 布林
如果值為true,那麼在連線關閉時會將臨時結果集合儲存下來,否則不儲存。
"output" : 字串
輸出集合的名稱,如果設定了這項,系統會自動設定keeptemp : true。
"query" : 文件
在發往map函式前,先用指定條件過濾文件。
"sort" : 文件
在發往map函式前給文件排序(與limit一同使用非常有用)。
"limit" : 整數
在發往map函式的文件數量的上限。
"scope" : 文件
可以再Javascript程式碼中使用的變數。
"verbose" : 布林
是否記錄詳細的伺服器日誌。