MongoDB權威指南(5)- 聚合

jopen 12年前發布 | 38K 次閱讀 MongoDB NoSQL數據庫

除了基本的查詢功能外,mongoDB還提供了聚合工具,從簡單的計數到使用MapReduce進行復雜數據的分析等。

1.count

最簡單的聚合工具就是count了,它返回document的數量

>  db.foo.count()
0
>  db.foo.insert({ " x "  :  1 })
>  db.foo.count()
1

也可以傳遞一個查詢條件,計算符合條件的結果個數

>  db.foo.insert({ " x "  :  2 })
>  db.foo.count()
2
>  db.foo.count({ " x "  :  1 })
1


2.distinct

distinct命令返回指定的key的所有不同的值。你必須指定一個collection和一個key。

>  db.runCommand({ " distinct "  :  " people " " key "  :  " age " })

假設我們的collection里的document是這樣子的:

{ " name "  :  " Ada " " age "  :  20 }
{
" name "  :  " Fred " " age "  :  35 }
{
" name "  :  " Susan " " age "  :  60 }
{
" name "  :  " Andy " " age "  :  35 }

那么返回的結果就是

>  db.runCommand({ " distinct "  :  " people " " key "  :  " age " })
{
" values "  : [ 20 35 60 ],  " ok "  :  1 }


3.group

group提供了更加復雜的聚合功能,它跟SQL里邊的group by很類似,你需要指定一個group by的key,mongoDB按照這個key的值把collection分成不同的組,經過聚合后每個組都產生一個結果document。

假設我們有一個站點用來跟蹤股票價格,從上午10點到下午4點,每隔幾分鐘就會有最新的股票價格存儲進數據庫,作為報表程序的一部分,我們想找出過去30天的收盤價,使用group就可以很容易做到。

股票價格的collection里有成千上萬條紀錄,格式如下:

{ " 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分組,找到每組里時間戳最新的記錄,把它放到結果集里

>  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;
...  }
... }}})
  • "ns" : "stocks"
    指定對哪個collection運行group命令
  • "key" : "day"
    指定按那個key進行分組
  • "initial" : {"time" : 0}
    累計器初始值,每個分組第一次調用reduce方法的時候傳遞給它的值,在一個分組里邊,始終使用同一個累計器,對累計器的修改會被保持下來。
  • "$reduce" : function(doc, prev) { ... }
    collection里的每個document,都要對之調用reduce方法,傳遞兩個參數給它,第一個是當前的document,第二個是累計器 document,累計器document就是到目前為止分組內的計算結果。(ps:不知道它為啥起個名字叫prev,使用total啊 accumulation之類的不是更容易理解些,我一眼看上去還以為是前一個document。)我們這個例子里,使用reduce方法來比較當前的 document和累計器document的時間,如果當前document的時間更靠后些的話,就是用當前document的值替換累計器 document的值。因為每個組都有各自的累計器,勿需擔心日期的不同對累計器的影響。

先前我們說的是取最近30天的價格,我們可以加一個條件,滿足條件的才會處理

>  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;
... }},
... 
" condition "  : { " day "  : { " $gt "  :  " 2010/09/30 " }}
... }})

如果某些document沒有day這個鍵的話,它們就會被歸入到day:null這個組,你可以給condition加個條件"day" : {"$exists" : true}來排除這個組。

使用終結器(Finalizer)

終結器用于最小化從數據庫到用戶的數據,我們看一個博客的例子,每篇博客都有幾個標簽,我們想找出每天最流行的標簽是什么。那么我們按照日期進行分組,對每個標簽計數:

>  db.posts.group({
... 
" key "  : { " tags "  :  true },
... 
" initial "  : { " tags "  : {}},
... 
" $reduce "  :  function (doc, prev) {
... 
for  (i  in  doc.tags) {
... 
if  (doc.tags[i]  in  prev.tags) {
...  prev.tags[doc.tags[i]]
++ ;
...  } 
else  {
...  prev.tags[doc.tags[i]] 
=   1 ;
...  }
...  }
... }})

返回的結果是下邊這個樣子

[
{
" day "  :  " 2010/01/12 " " tags "  : { " nosql "  :  4 " winter "  :  10 " sledding "  :  2 }},
{
" day "  :  " 2010/01/13 " " tags "  : { " soda "  :  5 " php "  :  2 }},
{
" day "  :  " 2010/01/14 " " tags "  : { " python "  :  6 " winter "  :  4 " nosql " 15 }}
]

實際上我們需要的只是值最大的那個標簽,并不需要將整個tags返回給客戶端,這就是group命令里可選的鍵"finalize"存在的原因。 finalize指定一個函數,在結果返回給客戶端之前,每個分組都會執行一次這個函數。我們使用finalize來去掉不需要的部分。

>  db.runCommand({ " group "  : {
... 
" ns "  :  " posts " ,
... 
" key "  : { " tags "  :  true },
... 
" initial "  : { " tags "  : {}},
... 
" $reduce "  :  function (doc, prev) {
... 
for  (i  in  doc.tags) {
... 
if  (doc.tags[i]  in  prev.tags) {
...  prev.tags[doc.tags[i]]
++ ;
...  } 
else  {
...  prev.tags[doc.tags[i]] 
=   1 ;
...  }
...  },
... 
" finalize "  :  function (prev) {
... 
var  mostPopular  =   0 ;
... 
for  (i  in  prev.tags) {
... 
if  (prev.tags[i]  >  mostPopular) {
...  prev.tag 
=  i;
...  mostPopular 
=  prev.tags[i];
...  }
...  }
... 
delete  prev.tags
... }}})

使用函數作為分組key

有些情況下,你可能需要更復雜的分組規則,不是一個簡單的key,那么你就可以用"$keyf"來定義一個分組函數。

>  db.posts.group({ " ns "  :  " posts " ,
... 
" $keyf "  :  function (x) {  return  x.category.toLowerCase(); },
... 
" initializer "  : ... })


4.MapReduce

MapReduce可是聚合工具里的高級武器,其他工具能做的它能做,其他工具做不了的它也能做。MapReduce是一個在多個服務器間可以并行執行的聚合方法,它將問題分割成多個塊,發送給不同的機器,讓每個機器解決自己的部分,當所有的機器都完成之后,把所有的結果都合并起來。(ps:這說的貌似 MapReduce最原初的概念,感覺跟我們下邊的內容關系不大)

MapReduce分兩步完成,第一步是映射(Map),將document里的鍵值投射為一組其他的鍵值對,第二步是精簡(Reduce),將投射出來的鍵值對按照鍵合并,每個鍵最后只有一個值。(ps:這是我的理解,書上寫的太拗口)

使用MapReduce的代價是速度,group的速度就不咋地,MapReduce更慢,所以一般都是作為后臺任務執行,完成之后對其結果collection進行查詢。

例子1:找出collection里所有的key

使用MapReduce解決這個問題確實是殺雞用牛刀,我們主要是看看MapReduce是如何工作的。MongoDB是無結構的,所以它不會跟蹤 document里都有哪些key,我們在這個示例里對collection里的每個key的使用次數進行計數,不包括嵌入的document的key。

第一步,映射(Map)使用一個特殊的函數來返回值,這些值后邊接下來處理,這個特殊函數就是emit。emit給MapReduce一個key和一個 value,我們這個例子里,我們將document的每個key投射為一個記錄其出現次數的數量{count : 1},因為我們要分別記錄每個key的出現次數,所以就需要對每個key調用emit函數。

>  map  =   function () {
... 
for  ( var  key  in   this ) {
...  emit(key, {count : 
1 });
... }};

現在我們就有了很多的{count : 1},每個都和collection里的一個key關聯,相同key的這些{count : 1}構成一個數組被傳遞給reduce函數,reduce函數有兩個參數,第一個是key,就是emit的第一個參數,第二個是數組,包含了被投射在這個 key上的所有{count : 1} 。

>  reduce  =   function (key, emits) {
... total 
=   0 ;
... 
for  ( var  i  in  emits) {
...  total 
+=  emits[i].count;
... }
... 
return  { " count "  : total};
... }

對來自映射階段或者前邊的reduce階段的結果,reduce函數必須能夠對其重復調用,所以reduce返回的document必須能夠重新傳遞給reduce函數(作為第二個參數)。

MapReduce函數的調用結果如下:

>  mr  =  db.runCommand({ " mapreduce "  :  " foo " " map "  : map,  " reduce "  : reduce})
{
" result "  :  " tmp.mr.mapreduce_1266787811_1 " ,
" timeMillis "  :  12 ,
" counts "  : {
" input "  :  6
" emit "  :  14
" output "  :  5
},
" ok "  :  true
}
  • "result" : "tmp.mr.mapreduce_1266787811_1"
    存儲MapReduce結果的collection的名字,這是個臨時的collection,連接關閉后即被刪除。我們可以指定一個好聽點的名字,并將這個collection永久保存,稍后會講到。
  • "timeMillis" : 12
    操作花費的時間,單位毫秒
  • "counts" : { ... }
    "input" : 6    傳遞給map函數的document數量
    "emit" : 14    map函數中調用emit函數的次數
    "output" : 5   結果集collection中document的數量

對結果集collection執行查詢就可以看到所有的key和出現次數了

>  db[mr.result].find()
" _id "  :  " _id " " value "  : {  " count "  :  6  } }
" _id "  :  " a " " value "  : {  " count "  :  4  } }
" _id "  :  " b " " value "  : {  " count "  :  2  } }
" _id "  :  " x " " value "  : {  " count "  :  1  } }
" _id "  :  " y " " value "  : {  " count "  :  1  } }


例子2:對網頁分類

假設我們有個網站,用戶可以提交通向其他頁面的鏈接,用戶可以給鏈接添加一些標簽標明這個鏈接和特定的主題關聯,如"政治","極客”,"icanhascheezburger"等。(ps:icanhascheezburger是個網站,主題內容是些搞笑的貓咪圖片,配些文字說明)我們可以用MapReduce找出那些主題是最近流行的。

首先,我們需要一個map函數,根據流行程度和最新程度將標簽投射為一個值。

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 "  : 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;
};

這樣,結果集里就包含了每個標簽的一個url列表和一個標識其流行度的總得分。

ps:

我們和關系數據庫比較一下更容易看到它的關鍵之處,關鍵之處就在于emit函數的第一個參數,sql中使用"group by 字段"進行分組,字段的每個不同值就是一個組,而MapReduce中是使用emit為每個字段不同值創建一個key和一個值的數組。簡單說,sql的 group by用的是字段名,emit用的是字段的值。明顯MapReduce更加靈活強大一些。


mongoDB和MapReduce

使用MapReduce命令,除了指定mapreduce,map,reduce這三個必須的鍵之外,還有很多其他的可選的鍵。

  • "finalize" : function
    終結器函數,接受reduce的輸出
  • "keeptemp" : boolean
    連接關閉后是否保存臨時結果集collection
  • "output" : string
    輸出collection的名字,使用此選項意味著keeptemp為true
  • "query" : document
    查詢條件,過濾傳遞給map函數的document
  • "sort" : document
    發送給map函數前對document進行排序,經常是和limit聯用
  • "limit" : integer
    發送給map函數的document的最大數量
  • "scope" : document
    在javascript代碼中可以使用的變量
  • "verbose" : boolean
    是否輸出更詳細的服務器日志

使用scope

如果在MapReduce中使用客戶端的值,那就必須使用scope選項了。你只需要傳遞給scope一個 變量名:值 格式的document就可以了,然后這個值在map,reduce以及finalize函數中就可以使用了。這個變量的值在各個函數中是只讀的。

比如,剛才我們第二個例子中計算頁面的最新性時使用的是1/(new Date() - this.date),如果我們想不使用new Date(),而是把當前日期傳遞進來的話,就可以定義個叫now的變量

>  db.runCommand({ " mapreduce "  :  " webpages " " map "  : map,  " reduce "  : reduce,
" scope "  : {now :  new  Date()}})

然后在map函數里就可以用1/(now - this.date)了。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!