[Golang] aggregate in mongo
在探討整個主題之前,我們先設計一個簡單的 schema 來符合這次
假設今天在資料庫內有主要結構,分別是 User 跟 Pod
User 非常簡單,就是描述一個使用者
Pod 這邊不用在意他是什麼東西,他是一個資源,然後透過 User 創立的,所以每個 Pod 裡面都會有一個欄位去記錄是哪個 User 創立的。
有了前述的假設之後,接下來想像一個實際需求
- 有一個
UserA創造了數百個Pod資源。 - 想要 Query 該
UserA並且列出該User創造的所有Pod資源的細部資料

在以前 SQL 的時候,我們可能會透過 SELECT 搭配 JOIN 的方式來存取,那今天假如在 mongoDB 的環境下,我們可以怎麼處理?
本文章的所有範例程式碼都可以在mgo Aggregate Example找到
必須要注意, mongodb 的版本至少要 > 3.0 之後才支援本文描述的 Aggregate
Structure
這邊就採用 golang 簡單的描述一下我們的結構,這些結構會用在 mongodb 裡面
type User struct {
ID bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
Pods []Pod `bson:"pods"`
}
type Pod struct {
ID bson.ObjectId `bson:"_id"`
CreatedBy bson.ObjectId `bson:"createdBy"`
Name string `bson:"name"`
}
Find
第一種方法其實非常直覺
- 就是先取得該
User的實例 - 去
Pod的Collection內進行搜尋,找出所有的Pod其記錄的User是UserA.
假設我們已經有該 User 的物件了,接下來很簡單透過 find 的方式找出所有由該 User 所創造的 Pod.
並且把找到的結果都放到 User 物件裡面的 Pods 變數
c := db.C("pods")
c.Find(bson.M{"createdBy": user.ID}).All(&user.Pods)
Aggregate
在 mgo裡面,如果想要做到 Aaggregate 類似這種 Join 的方式,其實可以透過一個叫做 pipeline 的函式來完成
為了使用 pipeline 的,我們必須要先描述我們想要如何去尋找這些資料,這些資料是由bson.M{} 陣列組成的。
該陣列內,至少要提供兩種不同的查詢方式
- $match: 這邊用來描述我們要如何查詢
User物件,在此範例中我們使用特定的User.ID來描述該User - $lookup: 用來描述要如何
Aggregate, 其中有四個欄位要填寫- from: 用來進行
Aggregate的collection名稱,此範例中我們設定為pods這個collection. - localField: 在該
pipeline要使用的collection內要使用的欄位,這邊我們會使用User裡面的_id. - foreignField: 在
from這個collection裡面要使用的欄位,此範例就是Pods裡面的createdBy. - as: 這個是用來存放查詢後的資料,首先其本身必須是個
Array,同時該欄位必須定義在User結構裡面。
- from: 用來進行
程式碼如下,該程式碼的意思很簡單,我們對 users 這個 collection 裡面透過 $match 去尋找特定的 User,同時透過 $lookup 的方式到 pods 該 collection 裡面去把 User.ID == Pods.CreatedBy 相同的 pods 都找出來,最後放到 user.pods 裡面
c := db.C("users")
pipeline := []bson.M{
{"$lookup": bson.M{"from": "pods", "localField": "_id", "foreignField": "createdBy", "as": "pods"}},
{"$match": bson.M{"_id": user.ID}},
}
var resp User
err := c.Pipe(pipeline).One(&resp)
if err != nil {
fmt.Println(err)
}
Performance
因為這次的範例非常簡單,所以並不能用來代表在所有的使用情境下的結果,只能用來表示如本文的情境下的效率問題。
在上述的範例專案內,一開始會先隨機產生一個使用者,並且隨機產生30000 筆 Pod 資料,並且嘗試比較不同數量的情況下,透過 Find 以及 Aggregate 實際上的效能問題。
下列的時間都是 (ms)
| Methond\Numbers | 1000 | 5000 | 10000 | 30000 | 50000 |
|---|---|---|---|---|---|
| Find | 5.7 | 18 | 35 | 111 | 250 |
| Aggregate | 7.4 | 22 | 77 | 180 | NaN |
這邊要特別注意的是,當 Pods 的數量過高的時候,使用 pipeline 來處理就會得到下列的錯誤訊息
Total size of documents in pods matching { $match: { $and: [ { createdBy: { $eq: ObjectId('5b86dac74807c532d70bea52') } }, {} ] } } exceeds maximum document size
根據官網描述,預設的情況下,只能存放 16MB 的資料。
這意味者資料過多的情況下,使用者要注意這個現象,避免資料存取失敗。
Summary
本文假設的情境非常簡單,基本上兩種方法都可以完成,但是在資料多寡的情況下,花費的時間就有所區別。
若是有更複雜的需求,在 pipeline 裡面還可以設定除了 $lookup 以及 $match 以外的用法,透過一次的呼叫就把資料給查詢完畢。
至少如果只是本文這種很簡單的情境,其實自己額外查詢即可,不論在簡潔性跟效率上我認為都更高。
所以還是一樣,不同情境下,每個功能都會有不同的使用方法跟考量點。 這邊就讓各位自己去評估囉