1. 程式人生 > >mongodb進階二之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" : 布林

是否記錄詳細的伺服器日誌。