[Golang] aggregate in mongo

在探討整個主題之前,我們先設計一個簡單的 schema 來符合這次
假設今天在資料庫內有主要結構,分別是 UserPod
User 非常簡單,就是描述一個使用者
Pod 這邊不用在意他是什麼東西,他是一個資源,然後透過 User 創立的,所以每個 Pod 裡面都會有一個欄位去記錄是哪個 User 創立的。

有了前述的假設之後,接下來想像一個實際需求

  1. 有一個 UserA 創造了數百個 Pod 資源。
  2. 想要 Query 該 UserA 並且列出該User創造的所有Pod資源的細部資料

Imgur

在以前 SQL 的時候,我們可能會透過 SELECT 搭配 JOIN 的方式來存取,那今天假如在 mongoDB 的環境下,我們可以怎麼處理?

本文章的所有範例程式碼都可以在mgo Aggregate Example找到

必須要注意, mongodb 的版本至少要 > 3.0 之後才支援本文描述的 Aggregate

Structure

這邊就採用 golang 簡單的描述一下我們的結構,這些結構會用在 mongodb 裡面

1
2
3
4
5
6
7
8
9
10
11
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

第一種方法其實非常直覺

  1. 就是先取得該 User 的實例
  2. PodCollection 內進行搜尋,找出所有的 Pod 其記錄的UserUserA.

假設我們已經有該 User 的物件了,接下來很簡單透過 find 的方式找出所有由該 User 所創造的 Pod.
並且把找到的結果都放到 User 物件裡面的 Pods 變數

1
2
c := db.C("pods")
c.Find(bson.M{"createdBy": user.ID}).All(&user.Pods)

Aggregate

mgo裡面,如果想要做到 Aaggregate 類似這種 Join 的方式,其實可以透過一個叫做 pipeline 的函式來完成

為了使用 pipeline 的,我們必須要先描述我們想要如何去尋找這些資料,這些資料是由bson.M{} 陣列組成的。
該陣列內,至少要提供兩種不同的查詢方式

  1. $match: 這邊用來描述我們要如何查詢 User 物件,在此範例中我們使用特定的User.ID來描述該 User
  2. $lookup: 用來描述要如何 Aggregate, 其中有四個欄位要填寫
    • from: 用來進行 Aggregatecollection 名稱,此範例中我們設定為 pods 這個 collection.
    • localField: 在該 pipeline 要使用的 collection 內要使用的欄位,這邊我們會使用 User 裡面的 _id.
    • foreignField: 在 from 這個 collection 裡面要使用的欄位,此範例就是 Pods 裡面的 createdBy.
    • as: 這個是用來存放查詢後的資料,首先其本身必須是個Array,同時該欄位必須定義在 User 結構裡面。

程式碼如下,該程式碼的意思很簡單,我們對 users 這個 collection 裡面透過 $match 去尋找特定的 User,同時透過 $lookup 的方式到 podscollection 裡面去把 User.ID == Pods.CreatedBy 相同的 pods 都找出來,最後放到 user.pods 裡面

1
2
3
4
5
6
7
8
9
10
11
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

因為這次的範例非常簡單,所以並不能用來代表在所有的使用情境下的結果,只能用來表示如本文的情境下的效率問題。

在上述的範例專案內,一開始會先隨機產生一個使用者,並且隨機產生30000Pod 資料,並且嘗試比較不同數量的情況下,透過 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 以外的用法,透過一次的呼叫就把資料給查詢完畢。
至少如果只是本文這種很簡單的情境,其實自己額外查詢即可,不論在簡潔性跟效率上我認為都更高。

所以還是一樣,不同情境下,每個功能都會有不同的使用方法跟考量點。
這邊就讓各位自己去評估囉

Reference