讀古今文學網 > MongoDB實戰 > 5.4 詳解聚合 >

5.4 詳解聚合

本節我將對MongoDB的聚合函數做詳細說明。

5.4.1 maxmin

通常總是需要找到給定集合裡的最大和最小值。使用SQL的數據庫提供了minmax函數,但MongoDB沒有這樣的函數,我們必須自己實現。要找到某個字段中的最大值,可以按照該字段降序排序,並限制結果集為一個文檔;按照相反順序排序就能取到對應的最小值。例如,如果希望找到投票數最多的評論,查詢需要對投票的字段進行排序,限制返回一個文檔:

db.reviews.find({}).sort({helpful_votes: -1}).limit(1)
  

返回文檔中的helpful_votes字段包含了該字段中的最大值。要獲取最小值,只要逆序排列就行了:

db.reviews.find({}).sort({helpful_votes: 1}).limit(1)
  

如果要在生產環境中發起查詢,helpful_votes字段最好能有一個索引。如果想獲得特定產品裡投票數最多的評論,則需要一個product_idhelpful_votes的復合索引。如果不清楚這麼做的原因,可以閱讀第7章。

5.4.2 distinct

MongoDB的distinct命令是獲取特定字段中不同值列表的最簡單工具。該命令既適用於單鍵,也適用於數組鍵。distinct默認覆蓋整個集合,但也可以通過查詢選擇器進行約束。

可以像下面這樣使用distinct獲取產品集合裡所有唯一標籤的列表:

db.products.distinct(\"tags\")
  

這很簡單。如果希望操作products集合的一個子集,可以傳入一個查詢選擇器作為第二個參數。這裡的查詢將不同的標籤值限定到Gardening Tools分類裡的產品:

db.products.distinct(\"tags\",
          {category_id: ObjectId(\"6a5b1476238d3b4dd5000048\")})
  

聚合命令限制

在實用性方面,distinctgroup有一個很大的限制:它們返回的結果集不能超過 16 MB。16 MB的限制並不是這些命令本身所強加的閾值,這是所有的初始查詢結果集大小。distinctgroup是以命令的方式實現的,也就是對特殊的$cmd集合的查詢,它們賴以生存的查詢則受制於該限制。如果distinctgroup處理不了你的聚合結果集,那麼就只能使用map-reduce代替了,它的結果可以保存在集合中而非內聯(inline)返回。

5.4.3 group

groupdistinct一樣,也是數據庫命令,因此它的結果集也受制於同樣的16 MB響應限制。而且,為了減少內存消耗,group不會處理多於10 000個唯一鍵。如果聚合操作在此範圍內,group是個不錯的選擇,因為通常情況下它會比map-reduce快。

我們已經看過根據用戶對評論分組的例子了,那個示例只能算「半個」。讓我們快速回顧一下傳遞給group的選項。

  • key,描述分組字段的文檔。舉例來說,要根據category_id分組,可以將{category_id: true}作為鍵。此處還可以使用復合鍵,比如,若想根據user_idrating對一系列帖子做分組,鍵看起來是這樣的:{user_id: true, rating: true}。除非使用keyf,否則key選項是必需的。

  • keyf,這是一個JavaScript函數,應用於文檔之上,為該文檔生成一個鍵,當用於分組的鍵需要計算時,這個函數非常有用。舉例來說,如果想根據每個文檔創建時是周幾來對結果集進行分組,但又不實際存儲該值,就可以用鍵函數來生成這個鍵:

function(doc) {
  return {day: doc.created_at.getDay;
}
  

這個函數會生成類似{day: 1}這樣的鍵。請注意,如果沒有指定標準的key,那麼keyf是必需的。

  • initial,作為聚合結果初始值的文檔。reduce函數第一次運行時,該初始文檔會作為聚合器的第一個值,通常會包含所有要聚合的鍵。舉例來說,如果正在為每個分組項計算總投票數和總文檔數,那麼初始文檔看起來是這樣的:{vote_sum: 0.0, doc_count: 0}

請注意,該參數是必需的。

  • reduce,用於執行聚合的JavaScript函數。該函數接受兩個參數:正被迭代的當前文檔和用於存儲聚合結果的聚合器文檔。聚合器的初始值就是初始文檔。下面是一個聚合投票和文檔總數的reduce函數示例:
function(doc, aggregator) {
  aggregator.doc_count += 1;
  aggregator.vote_sum += doc.vote_count;
}
 

請注意,reduce函數並不返回任何內容,它只不過是修改聚合器對象。reduce函數也是必需的。

  • cond,過濾要聚合文檔的查詢選擇器。如果不希望分組操作處理整個集合,就必須提供一個查詢選擇器。例如,假設只想聚合那些擁有五個以上投票的文檔,可以提供以下查詢選擇器:{vote_count: {$gt: 5}}

  • finalize,在返回結果集之前應用於每個結果文檔的JavaScript函數。該函數支持對分組操作的結果進行後置處理。我們通常會用它計算平均值,在分組結果的現有值之外,再加另一個值來保存平均值:

function(doc) {
  doc.average = doc.vote_count / doc.doc_count;
}
  

誠然,group有這麼多選項,上手比較麻煩。但是,稍加實踐之後,你會很快習慣的。

5.4.4 map-reduce

既然groupmap-reduce提供了類似的功能,你可能會想MongoDB為什麼要同時對它們提供支持呢?其實,在添加map-reduce之前,group是MongoDB唯一的聚合器,map-reduce是後來出於一些原因加入的。首先,MapReduce風格的操作正在成為主流,而且將這種思考方式融入產品之中看起來是很明智的。1其次,也是更實際的原因:對大數據集進行迭代,尤其是在分片配置中,需要有分佈式的聚合器,而MapReduce(範式)恰恰提供所需的內容。

1. 很多開發者是在谷歌那篇著名的關於分佈式計算的論文(http://labs.google.com/papers/mapreduce.html)裡初次看到MapReduce的。其中的思想後來成了Hadoop的基礎,而Hadoop是一個使用分佈式MapReduce處理大數據集的開源框架。之後MapReduce的思想得到了廣泛傳播,例如CouchDB就用MapReduce的範式來聲明索引。

map-reduce包含很多選項。此處詳細對這些選項做了說明。

  • map,應用於每個文檔之上的JavaScript函數。該函數必須調用emit來選擇要聚合的鍵和值。在函數上下文中,this的值指向當前文檔。例如,假設想根據用戶ID對結果分組,計算出總投票數和總文檔數,映射函數應該是這樣的:
function {
  emit(this.user_id, {vote_sum: this.vote_count, doc_count: 1});
}
  
  • reduce,一個JavaScript函數,接受一個鍵和一個值列表。該函數對返回值的結構有嚴格要求,必須總是與values數組所提供的結構一致。reduce函數通常會迭代一個值的列表,在此過程中對其進行聚合。回到我們的示例,以下展示如何處理映射函數輸出的內容:
function(key, values) {
  var vote_sum = 0;
  var doc_sum = 0;

  values.forEach(function(value) {
    vote_sum += value.vote_sum;
    doc_sum += value.doc_sum;

  });
  return {vote_sum: vote_sum, doc_sum: doc_sum};
}
  

請注意,通常在聚合過程中不會用到key參數的值。

  • query,用於過濾映射處理的集合的查詢選擇器。該參數的作用與groupcond參數相同。

  • sort,對於查詢的排序。與limit選項搭配使用時非常有用,這樣就可以對1000個最近創建的文檔運行map-reduce

  • limit,一個整數,指定了查詢和排序的條數。

  • out,該參數決定了如何返回輸出內容。要將所有輸出作為命令本身的結果,傳入{inline: 1}。請注意,這僅適用於結果集符合16 MB返回限制的情況。

另一個選擇是將結果放到一個輸出集合裡。此時,out的值必須是一個字符串,標明用於保存結果的集合的名稱。

將結果保存到輸出集合時有一個問題:如果最近運行過類似的map-reduce,那麼可能會覆蓋現有數據。因此,還有兩個集合輸出選項:一個用於合併結果和老數據,另一個對數據進行reduce處理。在合併的場景中,使用{merge: \"collectionName\"},新結果會覆蓋擁有相同鍵的現有項。如果使用{reduce: \"collectionName\"},會調用reduce函數根據新值來處理現有鍵的值。尤其是在執行要反覆運行的MapReduce任務時,希望把新數據整合到已有的聚合之中,reduce格外有用。在對集合執行新的MapReduce任務時,只需簡單添加一個查詢選擇器來限制聚合所需的數據集。

  • finalize,一個JavaScript函數,在reduce階段完成後會應用於每個返回的文檔上。

  • scope,該文檔指定了mapreducefinalize函數可全局訪問的變量的值。

  • verbose,一個布爾值,為true時,在命令返回文檔中會包含對map-reduce任務執行時間的統計信息。

在考慮使用MongoDB的map-reducegroup時,還有一個重要的限制需要引起注意:速度。對於大的數據集,這些聚合函數通常執行起來滿足不了用戶對速度的需求。這幾乎都要歸咎於MongoDB的JavaScript引擎。一個單線程解釋(非編譯)運行的JavaScript引擎是很難實現高性能的。

但也不要沮喪,map-reduce和group被廣泛使用於很多場景之中,並能充分勝任這些任務。對於那些還不適用的場景,則有其他方案,並有望在未來提供支持。其他方案是指在別處執行聚合,擁有大數據集的用戶已經在Hadoop集群上成功處理過數據。未來有望加入新的聚合函數,它們使用編譯的多線程代碼。這些功能計劃於MongoDB v2.0之後的某個時間發佈,你可以關注https://jira.mongodb.org/browse/SERVER-447。