知道了這些 MongoDB設計技巧,提高效率50%

範式化設計仍是反範式

考慮下這樣的場景,咱們的訂單數據是這樣的java

商品:
{
  "_id": productId,
  "name": name,
  "price": price,
}

訂單:
{
  "_id": orderId,
  "user": userId,
  "items": [
    productId1,
    productId2,
    productId3
  ]
}
複製代碼

當咱們查詢訂單內容的時候,先經過orderId查詢訂單,而後在經過訂單信息中的productId查詢到對應的商品信息。這種設計下一次查詢沒法獲取完整的訂單。mongodb

範式化結果就是讀取速度比較忙,當全部訂單的一致性會有保證。數組

在來看看反範式化設計bash

訂單:
{
  "_id": orderId,
  "user": userId,
  "items": [
   {
    "_id": productId1,
    "name": name,
    "price": price,
   },
   {
    "_id": productId2,
    "name": name,
    "price": price,
   },
  ]
}

複製代碼

這裏將商品信息做爲內嵌文檔存在訂單數據中,這樣當顯示的時候就只須要一次查詢就能夠了。ide

反範式讀取速度快,一致性稍弱,商品信息的變動不能原子性地更新到多個文檔。post

那麼咱們通常使用哪個呢?咱們在設計的時候要考慮如下問題優化

  1. 讀寫比是怎樣的?

可能讀取了商品信息一萬次才修改一次它的詳細信息,爲了那一次寫入快一點或者保證一致性,搭上一萬次的讀取消耗值得嗎?還有你認爲引用的數據多久會更新一次?更新越少,越適合反範式化。有些極少變化的數據幾乎根本不值得引用。好比名字,性別,地址等。spa

  1. 一致性重要嗎?

若是是確定的,則應該範式化。設計

  1. 要不要快速的讀取? 若是想要讀取儘量快,則要反範式化。在這個引用中就無所謂了,因此不能算考量因素,實時的應用要儘量地反範式化。

訂單文檔很是適合反範式化,由於其中的商品信息不常常變化。就算變了也沒必要更新到全部訂單。範式化再次就沒有什麼優點可言了。3d

因此本例中就是將訂單反範式化。

嵌入時間點數據

當一個商品打折或者換了圖片,並不須要更改原來的訂單中的信息。相似這種特定於某一時刻的時間點數據,都應該作嵌入處理。

在咱們上面提到的訂單文檔中有一處也是這樣,地址就屬於時間點數據。若某人更新了我的信息,那麼並不須要改變其以往的訂單內容。

千萬不要嵌入不斷增長的數據

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的轉換,若是你不這樣寫,就須要判斷每一個字段,而後進行填充。

想關注文章動態的能夠關注公衆號喲:

相關文章
相關標籤/搜索