MongoDB Advanced Schema Design Pattern - Bucket Pattern

用一句簡單的話說

將多筆資料聚合成一個文件(如時間序列)來減少 document 數量與提升存取效率。


場景

一對多且資料量大的時候,且 embedding 或 reference 都不是好選擇的時候,適合用在 IoT 場景,持續有資料進來的情況,也很常搭配 computed pattern 去計算統計資料的值。

好處如下:

  1. keep document size predictable
  2. read only data we need
  3. reduce number of documents in a collection
  4. improve index performance

要先評估最常被使用的 query 形式,藉此判斷 bucket 的粒度多細

例如這邊用 Book 有多個 user views 的例子,每次被瀏覽就會產生一個 document,共有多個 view document 要存,每個長這樣

{
_id: ObjectId('65b2cd4b297ec5dd6d75e5fa'),
book_id: 34538758,
timestamp: ISODate('2023-10-01T00:02:00.000Z'),
user_id: 65536
}

這邊的場景是,最常用的 query 需要計算每月的資料,所以量身定做 bucket 要放的內容,每個 bucket document 裏有每本書每個月的 view 資料存在 views collection

{
"id": {
"book_id": 1,
"month": {
"$date": "2023-11-22T00:00:00.000Z"
}
},
"views": [
{
"user_id": "user00",
"timestamp": {
"$date": "2023-11-22T00:00:00.000Z"
}
},
{
"user_id": "user10",
"timestamp": {
"$date": "2023-11-22T00:01:00.000Z"
}
},
// ...
]
}

為了防止 views field 變成 unbounded array,可以透過 schema validation tool 或者寫一些邏輯在 App code 裡面,都能讓 document 有個可預測的 size,例如 views 超過某個 threshold 之後就另開一個 bucket


之後再透過指定 book_id 的方式找到統計資料

db.views.find({
"id.book_id": 31459,
"id.month": ISODate("2023-09-30T00:00:00.000Z")
},
{
_id: 0,
views_count: {$size: "$views"}
}
)

// output:
{
"views_count": 2
}

或者 aggregation pipeline 也可以

[
{
$group: {
_id: {
book_id: "$book_id",
month: {
$dateFromParts: {
year: { $year: "$timestamp" },
month: { $month: "$timestamp" },
day: 1,
},
},
},
views: {
$push: {
user_id: "$user_id",
timestamp: "$timestamp",
},
},
},
},
{
$set: {
views_count: { $size: "$views" },
},
}
]

這樣就完成了 bucket pattern 的實踐。