正規化化設計還是反正規化
考慮下這樣的場景,我們的訂單資料是這樣的
商品:
{
"_id": productId,
"name": name,
"price": price,
}
訂單:
{
"_id": orderId,
"user": userId,
"items": [
productId1,
productId2,
productId3
]
}
複製程式碼
當我們查詢訂單內容的時候,先通過orderId查詢訂單,然後在通過訂單資訊中的productId查詢到對應的商品資訊。這種設計下一次查詢無法獲取完整的訂單。
正規化化結果就是讀取速度比較忙,當所有訂單的一致性會有保證。
在來看看反正規化化設計
訂單:
{
"_id": orderId,
"user": userId,
"items": [
{
"_id": productId1,
"name": name,
"price": price,
},
{
"_id": productId2,
"name": name,
"price": price,
},
]
}
複製程式碼
這裡將商品資訊作為內嵌文件存在訂單資料中,這樣當顯示的時候就只需要一次查詢就可以了。
反正規化讀取速度快,一致性稍弱,商品資訊的變更不能原子性地更新到多個文件。
那麼我們一般使用哪一個呢?我們在設計的時候要考慮以下問題
- 讀寫比是怎樣的?
可能讀取了商品資訊一萬次才修改一次它的詳細資訊,為了那一次寫入快一點或者保證一致性,搭上一萬次的讀取消耗值得嗎?還有你認為引用的資料多久會更新一次?更新越少,越適合反正規化化。有些極少變化的資料幾乎根本不值得引用。比如名字,性別,地址等。
- 一致性重要嗎?
如果是肯定的,則應該正規化化。
- 要不要快速的讀取? 如果想要讀取儘可能快,則要反正規化化。在這個引用中就無所謂了,所以不能算考量因素,實時的應用要儘可能地反正規化化。
訂單文件非常適合反正規化化,因為其中的商品資訊不經常變化。就算變了也不必更新到所有訂單。正規化化再次就沒有什麼優勢可言了。
所以本例中就是將訂單反正規化化。
嵌入時間點資料
當一個商品打折或者換了圖片,並不需要更改原來的訂單中的資訊。類似這種特定於某一時刻的時間點資料,都應該做嵌入處理。
在我們上面提到的訂單文件中有一處也是這樣,地址就屬於時間點資料。若某人更新了個人資訊,那麼並不需要改變其以往的訂單內容。
千萬不要嵌入不斷增加的資料
MongoDB儲存資料的機制決定了對陣列不斷追加資料是很低效的。在正常使用中陣列和物件大小應該相對固定。
嵌入20,100,或者100000個子文件都不是問題,關鍵是提前這麼做,之後基本保持不變。否則放任文件增長會使得系統慢的你受不了。
對於那些不斷增加的內容,必須評論這個時候應該將其作為單獨的文件處理比較合適。
儘可能預先分配空間
只要知道文件開始比較小,後來會變為確定的大小就可以使用這種優化方法,一開始插入文件的時候,就用和最終資料大小一樣的垃圾資料填充,比如新增一個garbage欄位(其中包含一個字串,串大小與文件最終大小相同),然後馬上重置欄位
db.collection.insert({"_id" : 1,/* other fields */, "garbase": longString});
db.collection.update({"_id" : 1, });
複製程式碼
這樣,MongDB就會為文件今後的增長分配足夠的空間
mongodb中儲存文件是預留了空間的,允許文件擴容,但是當文件增大到一定地步的時候,就會超過原本分配的空間,此時文件就會進行移動
用陣列存放要匿名訪問的內嵌資料
一個常見的問題就是內嵌的資訊到底是用陣列還是用子文件存。如果確切知道要查詢的內容,就要用子文件。如果有時候不太清楚查詢的具體內容,就要用陣列。當知道一些條目的查詢條件時,通常該使用陣列。
假設我想記錄下遊戲中某些物品的屬性。我們可以這樣建模
{
"_id": 1,
"items" : {
"slingshot": {
"type" : "weapon",
"damage" : 30,
"ranged" : true
},
"jar" : {
"type": "container",
"contains": "fairy"
}
}
}
複製程式碼
假設要找出所有damage大於20的武器,子文件不支援這種查詢方式,你只能知曉具體某種物品的資訊才能查詢,比如{"items.jar.damage": {"$gt":20}}. 如果無需識別符號,就要用陣列
{
"_id": 1,
"items" : [
{
"id" : "slingshot"
"type" : "weapon",
"damage" : 30,
"ranged" : true
},
{
"id" : "jar",
"type": "container",
"contains": "fairy"
}
]
}
複製程式碼
比如{"items.damage":{"$gt":20}}
就行了。如果還需要多條件查詢,可以使用$elemMatch.
如何使用自增id代替ObjectId
有時候在使用過程中受限於業務或者其他情況,並不想使用ObjectId,而是想要使用自動Id來代替。但是MongoDB本身並沒有提供這個功能,那麼如何實現呢?
可以新建一個collection來儲存自增id
{
"_id" : ObjectId("59ed8d3df772d09a67eb25f6"),
"fieldName" : "user",
"seq" : NumberLong(100064)
}
複製程式碼
fieldName表示哪個集合,那麼下次要使用的時候只用取出這個值加1就可以了。程式碼如下
public Long getNextSequence(String fieldName, long gap) {
try {
Query query = new Query();
query.addCriteria(Criteria.where("fieldName").is(fieldName));
Update update = new Update();
update.inc("seq", gap);
FindAndModifyOptions options = FindAndModifyOptions.options();
options.upsert(true);
options.returnNew(true);
Counter counter = mongoTemplate.findAndModify(query, update, options, Counter.class);
if (counter != null) {
return counter.getSeq();
}
} catch (Throwable t) {
log.error("Exception when getNextSequence from mongodb", t);
}
return gap;
}
複製程式碼
不要到處使用索引
索引是很強大,但是要提醒你的是,不是所有查詢都可以用索引的。比如你要返回集合中90%的文件而非獲取一些記錄,就不應該使用索引。
如果對這種查詢用了索引,結果就是幾乎遍歷整個索引樹,把其中一部分,比方說40GB的索引都載入到記憶體。然後按照索引中的指標載入集合中200GB的文件資料,最終將載入 200GB + 40GB = 240GB的資料,比不用索引還多。
所以索引一般用在返回結果只是總體資料的小部分的時候。根據經驗,一旦要大約返回集合一般的資料就不要使用索引了。
若是已經對某個欄位建立了索引,又想在大規模查詢時不使用它(因為使用索引可能會較低效),可以使用自然排序來強制MongoDB禁用索引。自然排序就是“按照磁碟上的儲存順序返回資料",這樣MongDB就不會使用索引了。
db.students.find().sort({"$natural" : 1});
複製程式碼
如果某個查詢不用索引,MongoDB就會做全表掃描。
索引覆蓋查詢
如果你想要返回某些欄位且這些欄位都可以放到索引中,那麼MongoDB可以做索引覆蓋查詢,這種查詢不會訪問指標指向的文件,而是直接用索引的資料返回結果,比如有如下索引
db.students.ensureIndex(x : 1, y : 1, z : 1);
複製程式碼
現在查詢被索引的欄位,並要求只返回這些欄位,MongoDB就沒有必要載入整個文件
db.students.find({"x" : "xxx", "y" : "xxx"},{x : 1, y : 1, z : 1, "_id" : 0});
複製程式碼
注意由於_id是預設返回的,而它又不是索引的一部分,所以MongoDB就需要到文件中獲取_id,去掉它,就可以僅根據索引返回結果了。
若是查詢值返回幾個欄位,則考慮將其放到索引中,即使不對他們執行查詢,也能做索引覆蓋查詢。比如上面的欄位z。
AND查詢要點
假設要查詢滿足條件A、B、C的文件。滿足A的文件有40000,滿足B的有9000,滿足C的有200,要是讓MongoDB按照這個順序查詢,效率可不高。
如果把C放到最前,然後是B,然後是A,則針對B,C只需要查詢最多200個文件。
這樣工作量顯著減少了。要是已知某個查詢條件更加苛刻,則要將其放置到最前面。
OR型查詢要點
OR與AND查詢相反,匹配最多的查詢語句應該放到最前面,因為MongDB每次都要匹配不在結果集中的文件。
單表查詢儘量使用Respostory
開發中,對於簡單的查詢我一般使用MongoRepository來實現功能,如果有複雜的結合MongoTemplate,注意這兩者是可以混合使用的。
converter的建議
開發中我們要寫對於一個collection,其中有些特殊的型別(比如列舉)需要我們寫converter,大多時候是雙向的,比如db-->collection和collection-->db 如果只有一個型別需要轉換,我們可以針對這一個屬性進行轉換,比如下面的例子
@WritingConverter
@Component
public class UserStatusToIntConverter implements Converter<UserStatus, Integer> {
@Override
public Integer convert(UserStatus userStatus) {
return userStatus.getStatus();
}
}
@ReadingConverter
@Component
public class UserStatusFromIntConverter implements Converter<Integer, UserStatus> {
@Override
public UserStatus convert(Integer source) {
return UserStatus.findStatus(source);
}
}
複製程式碼
一個欄位還好,如果一個類中有很多個欄位都需要做轉換的話,就會產生很多個converter,這個時候我們可以寫一個類級別的轉換器
@ReadingConverter
@Component
public class OperateLogFromDbConverter extends AbstractReadingConverter<Document, OperateLog> {
@Override
public OperateLog convert(Document source) {
OperateLog opLog = convertBasicField(source);
if (source.containsKey("_id")) {
opLog.setId(source.getLong("_id"));
}
if (source.containsKey("module")) {
opLog.setModule(ModuleEnum.findModule(source.getInteger("module")));
}
if (source.containsKey("opType")) {
opLog.setOpType(OpTypeEnum.findOpType(source.getInteger("opType")));
}
if (source.containsKey("level")) {
opLog.setLevel(OpLevelEnum.findOpLevel(source.getInteger("level")));
}
return opLog;
}
private OperateLog convertBasicField(Document source) {
Gson gson = new Gson();
return gson.fromJson(source.toJson(), OperateLog.class);
}
}
複製程式碼
上面程式碼我用了GSON做common field的轉換,如果你不這樣寫,就需要判斷每個欄位,然後進行填充。
想關注文章動態的可以關注公眾號喲: