MongoDB Schema Design Anti Pattern
Unbounded Arrays
當 array 的 element 數量無止盡的增加,除了會造成 query 效能變差以外,還可能超過 document size 16 MB 的限制
如何避免?
- Only store data together if it is queried together.
- 元素數量龐大的 array 不應該被 embed
範例
book document 中有大量的 reviews,這是一個 unbounded array 我們不想看到,常用 subset patterns 或 extended reference pattern 處理,實際要 by case 判斷用什麼方式解決
// books collections |
Solution
extended reference pattern (這個 case 不是最佳解)
會將 book document 的 reviews field 移除並新增一個 reviews collection,並且在每個 review document 內依照實際使用場景加上 query 需要的資訊
// books collection |
因為 review document 含有太多與 book document 重複的 field,所以並不是一個很好的方式
subset pattern
將最常用的三個 review 留在 book document 裏,並新增 reviews collection 將所有 review document 放進去,這樣做重複的資料最多也就是每本書裡面最常用的三個 review
// books collections |
因為在原本的 book document 保留了最常用的三筆 review,在取得 review 時也大幅減少 $lookup 或是 multiple query 的場景
Bloated Documents
為了遵守黃金守則 data is accessed together should be stored together,在初期設計會將整個 document 塞滿必須的資訊,但當產品開始成長且使用場景增加了,容易造成同一個 document 儲存了可以分開讀取的資料,而在某些場景拖垮了效能
以 Book detail collection 舉例來說,在初期只需在 detail 頁面顯示每本書的資訊,但隨著產品發展多了一個需求是在首頁顯示所有書的 summary,也就是只需要顯示 document 中部分 field 的資訊,但現行的架構為了拿首頁需要的資訊需要連同其他資訊一起拿,導致了效能瓶頸
{ |
一個簡單的評估方式是,MongoDB 的預設 storage engine Wired Tiger 會將常使用的資料儲存在 memory 中,也就是 Wired Tiger Internal Cache,儲存的資訊包含 documents 和 index 等等,而儲存的上限粗估為 500 MB 上下
另外配合兩個指令來評估目前使用的 memory size
// documents 數量 |
兩者相乘,要是超過現有的 memory size,可以考慮兩種解法
- 增加 cluster 記憶體
- 修改 schema
為了幫公司省錢成為優質的工程師,從第二點下手,將原本的 detail collection 另外切出一個 summary collection 僅儲存 summary 必要的資訊
// summary collection |
而當首頁讀取 summary 資訊時就只需要從 summary collection 來讀取即可解決效能的問題
Massive number of collections
MongoDB 雖然沒有限制 collection 的數量,但超過一定數量依舊會影響效能,這取決於硬體的限制
MongoDB 的預設儲存引擎 WiredTiger 會將每個 collection 及對應的 index 分開儲存,M10 cluster 的用戶大概儲存 5000 個 collection 就會開始感受到效能阻礙,M20/M30 大概是 10000 個,Sharded Cluster 也大概是 10000 個/shard
解決方式
- 定期刪減 collections
- 重新設計 schema 設計的方式
- 做 sharding
定期檢查不常使用的 collection,將它們刪除或是做 archive,而當做完這些事情後 collection 數量依舊太多就要考慮重新思考 schema 設計的方式
以 book store 舉例來說:想追蹤每本書的觀看次數,將每本書的 view 儲存在各自的 collection 就容易造成 Massive number of collections
// book1_views collection |
這個 case 來說,會想要修改設計的方式將所有 view 存在同一個 collection 中已降低 collection 數量
// book_views |
Unnecessary Indexes
雖然 MongoDB 給每個 collection 最多的 index 數量可以到 64 個,但仍然建議使用越少 index 越好,過多的 index 容易造成效能瓶頸,可以從幾個面向觀察並減少 index 數量
- 未使用的 index
- 很少用的 index
- 被其他 index 涵蓋的 index
不恰當使用 index 有幾個可能會拖垮效能的地方
- index 被使用時會佔據記憶體用量
- 佔據的空間隨著 document 數量增加而變大
- create, update, delete document 時都會需要更新 index
可以利用 pipeline 檢查目前 collection 中 index 的使用狀態
const sortIndexesByAccessOpsPipeline = [ |
或在 MongoDB Atlas 中的 Data Explorer 或 Performance Advisor 中觀察 index 的使用狀況。
而當初 index 在建立時都會有原因,建議在刪除 index 之前先用 hideIndex()
的方式觀察一陣子後再刪除。
Data Normalization
在規劃 schema 時為了效能或減少資料重複為考量,有時會很直覺的將資料分組並存放在不同處,但有時這樣做會造成經常一起被 query 的資料分散在各處,導致需要多個 query 或必須用 $lookup 在不同資料分組間查詢,反而造成效能瓶頸。
Data Normalization 指的就是上述的情況。
例如一開始在分組時直覺地將 books collection
與 reviews collection
分開存放,但後來發現這些資料通常會一起被 query。與此同時也不可能將 reviews 直接 embed 進 books,因為容易造成 unbounded array。
經常被考量的解決方案有 subset pattern 或 extended reference pattern
subset pattern
當每本書一開始僅需要顯示前三筆 review 資訊,就可以將前三筆資訊直接放在 book documents 中
// books collection |
結果是
- 依舊能達到將資料分成不同的 collection 來呈現
- 造成了部分 review 資料重複在不同的 collection,但不再需要多個 query 或 $lookup 來查找資料
但也須注意 review 變更時,需要同時更新 books collection 與 reviews collection 裡 review 資訊的策略
extended reference pattern
如果需求改為需要顯示同一本書的所有 review,那麽 extended reference pattern 將會是比較好的選擇
在每個 review document 內存放部分的 book 資訊
// reviews collection |
結果也是
- 依舊能達到將資料分成不同的 collection 來呈現
- 造成了部分 review 資料重複在不同的 collection,但不再需要多個 query 或 $lookup 來查找資料
而且因為 book title 與 author 的資料幾乎不可能變更,這個場景下反而不用花太多心力考慮同個資料變更時要去更新所有 reference 的策略
Case-Sensitivity
MongoDB 的 query 預設是 case sensitive 的,Case-Sensitivity 指的是套用預設設定卻預期 case insensitive,除了會得到非預期的結果也可能影響效能。
可以利用 Collation 達成 case insensitive。
Collation 定義了在特定語言中如何比較與排序的規則,需要指定語言 ex: ‘en’
以及 strength,strength 預設是 3,這個值只會做 case sensitive 的查詢,想要達成 case insensitive 必須設定為 1 或 2
來看一下用法
db.Books.find({ author: 'Paul Done' }).collation({locale: 'en', strength: 2}) |
在 query 之中指定了 collation,這次的查詢會是不區分大小寫的
有兩個常見的方法來解決 anti pattern
方法一
指定帶有 collation 設定的 index 並且在 query 時使用相同設定的 collation,例如
// 建立 index |
不確定目前 index 是否有相同 collation 可以用 db.collection.getIndexes()
方法確認,如果目前的 index 沒有設定 collation 必須 dropIndex()
之後重新建立對應 collation 的 index
方法二
在建立 collection 時指定預設的 collation,此時
- 預設會套用在所有的 index 中而不須在建立 index 時額外只定
- 所有對於這個 collection 的 query 預設也會使用相同設定的 collation,可以在 query 時另外指定 collation 去覆寫,但不太建議
但要注意的是一旦 collection 設定好預設值後就無法改變
有時候也會使用 $regex 搭配 /i
option 來做 case insensitive 的查詢,但這麼做效能不好所以不推薦
如果有更進階的 query 需求可以查詢 MongoDB 的 Atlas Search