之前在看Mongo的書時,看到了聚合這章。其中談到了group這個功能,其實正如書中所說,MongoDB中的group和SQL中的group by是很相似的,但我自我分析,可能由於Mongo中的group的使用形式不同,而且使用的是js語法,所以導致咋一看上去不明白這個group怎么用。下面通過具體的一個例子來詳細說明Mongo的group用法。
我們平常所用的博客,每天會有很多人發博客,每篇文章中都有多個標簽,現在要找出每天最熱點的標簽。首先,我們可以按天分組,將每天每一標簽的計數給統計出來。
我們可以簡單地假設集合中文檔的結構如下:
1 {“title” : “java sun”, “author” : “jk”, “day” : “2012-12-14”, “tags” : [“java”, “nosql”, “spring”]} 2 3 {“title” : “SSH2的整合”, “author” : “cj”, “day” : “2012-5-10”, “tags” : [“struts2”, “hibernate”, “spring”]} 4 5 {“title” : “C#的高級用法”, “author” : “zt”, “day” : “2012-4-3”, “tags” : [“C#”, “SQL”]} 6 7 {“title” : “PHP Mongo”, “author” : “lx”, “day” : “2012-12-14”, “tags” : [“PHP”, “nosql”, “mongo”]} 8 9 …
Mongo代碼如下:
1 >db.posts.group({ 2 3 … “key” : {“day” : true}, 4 5 … “initial” : {“tags” : {}}, 6 7 … “$reduce” : function (doc, prev) { 8 9 … for (i in doc.tags) { 10 11 … if (doc.tags[i] in prev.tags) { 12 13 … prev.tags[doc.tags[i]]++; 14 15 … } 16 17 … else 18 19 … { 20 21 … prev.tags[doc.tags[i]] = 1; 22 23 … } 24 25 … } 26 27 … } 28 29 })
現在我們來逐行分析上述的代碼意思。
1 “key” : {“day” : true}
“key”表示集合數據分組的依據。這里我們指定了“day”鍵,那么會根據集合文檔中的發布博客時間來進行分組。那有人會問,那個“true”有什么意義?如果指定了{“day” : true},那么在分組的結果中就會顯示每組“day”的鍵值。
1 “initial” : {“tags” : {}}
這個大家可能會迷惑,initial是初始化的意思,分組為什么還要初始化呢?SQL好像也沒有類似的概念啊。這個就是兩者不同的地方了。這里是為了初始化累加器的鍵值,我們可以把這個所謂的“累加器”當作一個文檔,這個文檔中存放是的在分組過程中收集、計算出的信息,不一定是集合文檔中的原信息,這里需要注意。“tags”表示每個分組中第一個文檔對應調用“$reduce”指定函數時的參數初始化,后續該分組的文檔對應調用“$reduce”指定函數時,會不斷“累加”,保留住每次對“tags”所做的更新結果。有人可能不理解上面一句在講什么,可以結合下面的解釋來理解。
1 “$reduce” : function(doc, prev) { … }
看到這里可能有些人就徹底崩潰了,我也是,不就分個組嗎,怎么把函數都整出來,還是js的。其實我們可以把這里的函數理解為分組過程,在分組的過程中,我們又人為地添加了一些操作,比如信息收集、匯總、統計等等。“doc”代表分組過程中的每一個集合中的文檔,而“prev”則代表“累加器文檔”的累加狀態,當一個分組的組員划分完畢時,這個“prev”文檔中的鍵值對的最終狀態就是我們想要的結果。這里需要注意函數體中的“prev.tags”和“doc.tags”是不同的,不信我們可以看結果。
按照上面的代碼分類后得到的結果如下所示:
1 {“day” : “2012-12-14”, “tags” : {“java” : 1, “nosql” : 2, “spring” : 1, “mongo” : 1}} 2 3 {“day” : “2012-5-10”, “tags” : {“struts2” : 1, “hibernate” : 1, “spring” : 1}} 4 5 {“day” : “2012-4-3”, “tags” : {“C#” : 1, “SQL” : 1}}
看出不同了嗎?上面的結果中,“tags”鍵的值是一個內嵌文檔,而這對應的就是“pre.tags”。“doc.tags”表示的原集合文檔中“tags”鍵的多值數組。
我們要找的是每天最熱門的標簽,顯然我們上面的結果還有些多余的信息,那么就讓我們使用“finalize”鍵來進行精簡吧,還是先看代碼。
1 >db.posts.group({ 2 3 … “key” : {“day” : true}, 4 5 … “initial” : {“tags” : {}}, 6 7 … “$reduce” : function (doc, prev) { 8 9 … for (i in doc.tags) { 10 11 … if (doc.tags[i] in prev.tags) { 12 13 … prev.tags[doc.tags[i]]++; 14 15 … } 16 17 … else 18 19 … { 20 21 … prev.tags[doc.tags[i]] = 1; 22 23 … } 24 25 … } 26 27 … }, 28 29 … “finalize” : function (prev) { 30 31 … var mostPopular = 0; 32 33 … for (i in prev.tags) { 34 35 … if (prev.tags[i] > mostPopular) { 36 37 … prev.tag = i; 38 39 … mostPopular = prev.tags[i]; 40 41 … } 42 43 … } 44 45 … delete prev.tags; 46 47 … } 48 49 })
“finalize”附帶的函數,在每個分組結果傳遞到客戶端之前會被調用一次,從而可以對分組結果進行“修剪”。從上面我們可以看出,原先結果中的“tags”鍵值給刪去了,加上了一個“tag”鍵值,所以最終的結果如下:
1 {“day” : “2012-12-14”, “tag” : { “nosql”} 2 3 {“day” : “2012-5-10”, “tags” : {“struts2” : 1}} 4 5 {“day” : “2012-4-3”, “tags” : {“C#” : 1}}
在舉個例子,本來我在剛看過上面的那段代碼后,想在自己寫的示例系統——用戶管理系統中,用PHP結合group功能實現用戶的愛好分類統計,但由於剛開始我也一直處於迷糊狀態,所以一直沒想出解決辦法,今天通過仔細閱讀PHP開發手冊的例子,終於實現了這一功能。下面是用戶集合中的文檔結構:
1 { "_id" : ObjectId("50f0c0ca323365ec0c000006"), "username" : "大胖", "age" : 23, "birthday" : ISODate("1989-10-29T16:00:00Z"), "interest" : [ "籃球", "足球", "乒乓球", "高爾夫球" ] } 2 3 { "_id" : ObjectId("50f35b6f323365f00c000001"), "username" : "六福", "age" : 24, "birthday" : ISODate("1988-05-05T15:00:00Z"), "interest" : [ "乒乓球", "籃球" ] } 4 5 { "_id" : ObjectId("50f37257323365ec0c000000"), "username" : "七喜", "age" : 30, "birthday" : ISODate("1984-09-30T16:00:00Z"), "interest" : [ "足球", "橄欖球" ] } 6 7 …
那么如何用group方法來統計出喜愛籃球的有多少人,喜愛足球的有多少人等信息呢?現在我就用Mongo——PHP驅動提供的API來實現group的功能。有興趣的人可以根據下面的代碼簡練出Mongo語法的代碼。
1 /* 2 3 * 統計愛好分類信息 4 5 */ 6 7 public function groupByInterest() 8 9 { 10 11 $key = array(); //$key沒有指定分組依據,那么所有文檔認為屬於同一組 12 13 $initial = array('interests' => array()); 14 15 $reduce = 'function(obj, prev) { 16 17 for (i in obj.interest) 18 19 { 20 21 if (obj.interest[i] in prev.interests) 22 23 { 24 25 prev.interests[obj.interest[i]]++; 26 27 } 28 29 else 30 31 { 32 33 prev.interests[obj.interest[i]] = 1; 34 35 } 36 37 } 38 39 }'; 40 41 $g = $this->users->group($key, $initial, $reduce); 42 43 return $g['retval']; 44 45 }
注意上面代碼中的“$key = array();”,我沒有指定分組依據的鍵名,有人會問,這樣不會錯嗎?答案是不會的。當我們沒有指定分組依據的鍵名時,那么集合中所有文檔被認為屬於同一組。我之所以這么說是我從分組結果中分析出來的,但PHP開發手冊上的例子說的是如果在這種情況下,集合中的文檔是各自獨立成為一組的,但我堅持自己的說法,因為我看得到的結果是這樣的,如果有哪位朋友可以給個確切說明的話,歡迎指正。