mongodb進階二之mongodb彙總,mongodb進階彙總
上篇我們說了mongodb的進階查詢:http://blog.csdn.net/stronglyh/article/details/46817789
這篇說說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" : 布爾
是否記錄詳細的伺服器日誌。
感謝匯智網:http://hubwiz.com/
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。