MongoDB設計系列


原創文章,如果轉載請標明出處、作者。 https://www.cnblogs.com/alunchen/p/9762233.html

 

1 前言

MongoDB作為現今流行的非關系型文檔數據庫,已經有很多關於它的資料與介紹。

寫這篇文章時,MongoDB已經更新到4.0版本,支持事務型操作。還末在生產環境中使用事務型的MongoDB,因為功能還未完善。

好啦,說正題了。本文是總結本人使用MongoDB多年的經驗,有不同見解之處,請多多關照。

說明:

1)關系型SQL的表在MongoDB上稱為集合。為了好對比,這里MongoDB上說的表也是集合的意思。

2)這里mongodb與關系型SQL的對比之中設計上的,其它底層等對比這里不做任何的闡述。

2 對比

MongoDB與SQL的對比(對比的MongoDB都在使用WiredTiger存儲引擎為前提)

對比

 

3 設計

從設計來說,相信很多人都是使用過SQL設計表。

題外話:在以前,甚至是現在,很多架構設計都是從設計表開始,設計完表再開始寫代碼。現在呢?推崇代碼優先方案,或者實體優先方案。先設計實體,然后實體導入到數據庫中。這是典型的DDD模式。

下文中,為了更好理解,首先從SQL方面是怎么設計的,然后再看看MongoDB是怎么設計的,有什么難點。

3.1 設計:一對一的情況

前提:兩張表的一對一。

場景:鍵盤與主機的關系。一台主機有一個鍵盤,一個鍵盤有一台主機。

主機字段:id、CPU核數、內存大小、顯卡大小。

鍵盤字段:id、主機的id、鍵盤類型(機械/非機械)、顏色、牌子。

 

3.1.1 關系型SQL

設計:會有兩張表,一張為主機表,一張為鍵盤表。當然你可以合成一張表,合成一張表就沒有討論的價值了。如下圖:

一對一關系-sql

查詢時:

查詢主機與鍵盤數據時,即兩張表數據時,需要關聯兩張表查詢。性能比較慢。

查詢主機數據時,只需要單個表查詢。

更新時:

都是更新一張或兩張表數據。會鎖表行。

增加時:

增加一張或兩張表數據。

 

3.1.2 MongoDB

在MongoDB來說,是屬於文檔型數據庫,在一對一關系來說一般使用內嵌方式,因為性能體現在'以空間換取時間'。

拋棄設計,在一對一來說,可以使用內嵌,又可以使用關系型SQL的兩張表關聯。但是使用兩張表的關聯顯得累贅。

所以,如果使用關系型SQL的兩張表關聯,沒有什么對比性,並且MongoDB不推薦這樣的做法。這里只介紹內嵌。

設計:一對一,內嵌。

 

保存在MongoDB如下json:

{
    "主機id":"1",
    "CPU核數":"2核",
    "內存大小":"16GB",
    "顯卡大小":"2GB",
    "鍵盤":{
        "鍵盤類型":"機械",
        "顏色":"Black",
        "牌子":"雙飛燕"
    }
}

 

可以看到,鍵盤去掉了自己的id,並且去掉了關聯主機的id。

查詢/更新/增加時,一張表查詢即可。

 

小結                                                                                      

所以在一對一來說,MongoDB與關系型SQL沒有什么對比性。

硬要說對比性,一對一場景來說,MongoDB更加面向對象。

建議:在MongoDB一對一場景下,建議使用內嵌,不應該使用兩張表關聯。

 

3.2 設計:一對多的情況

在一對多中,存在者4種不同的場景,需要都一一介紹。因為最終設計都根據業務需求來的,所以這里舉不同的業務場景來說明。

 

3.2.1 場景1,完整內嵌型一對多

場景:訂單與訂單項的關系。

 

網購過的同學都知道,用戶支付訂單時,有很多個子訂單項。比如某寶的下單時,包括不同商鋪的不同的訂單項。

在訂單與訂單項關系中,一般查詢都是查詢整個訂單,沒有對單個訂單項的查詢。

 

想象一下以下場景:

    1)查看訂單的場景。進入某寶,查看訂單時,都是從訂單進入,然后查看所有的訂單項。

    2)支付成功或失敗后,修改訂單時,都是修改訂單,沒有修改訂單項。

 

字段:

訂單字段:id、訂單號、運費、總價格、訂單狀態。

訂單項字段:名稱、單價、數量。

 

MongoDB設計

從上面2個場景,很容易的想到完整內嵌型的一對多非常適合這種需求。

看看如下JSON:

{
    "id": "asdg184981651568956", 
    "訂單號": "201809270012598323334", 
    "運費": 0, 
    "總價格": 41, 
    "訂單狀態": "已經支付", 
    "訂單項": [
        {
            "名稱": "益達口香糖", 
            "單價": 8, 
            "數量": 2
        }, 
        {
            "名稱": "大大口香糖", 
            "單價": 5, 
            "數量": 1
        }, 
        {
            "名稱": "綠箭口香糖", 
            "單價": 10, 
            "數量": 2
        }
    ]
}

 

查詢訂單時:

查詢訂單一條語句即可,就能查詢出訂單以及訂單所有的訂單項。也不會需要查詢出單個訂單項。

修改訂單時:

只修改根內容,不會修改訂單項(內嵌的)內容。

 

小結                                                                                                                                                                                  

使用完整內嵌一對多設計時,前提有這幾點:

    1)內嵌數組大小不宜過大。因為太大會導致整個實體內容大,從而導致網絡延遲問題。建議不超過20或30個。具體根據內嵌實體大小而定。

    2)如果內嵌實體數組多(一般多於5個),查詢時,內嵌實體內容沒有作為單獨的實體查詢。例如不會有單個查詢訂單項的需求。

    3)如果內嵌實體數組多(一般多於5個),修改/刪除/插入時,內嵌實體內容沒有作為單獨的實體修改/刪除/插入。

 

3.2.2 場景2,內嵌id型一對多

場景:公司與員工的關系。一個公司對應個員工,雇佣關系。假設這里的公司最多都是幾千位員工。

想象一下以下場景:

    1)員工的屬性很多。

    2)一個公司有很多員工,可能也有很少員工。

    3)單獨查詢員工信息的場景。

    4)修改員工信息的場景。

綜上場景,使用3.2.1內嵌設計型不符合我們的需求。所以我們可以考慮內嵌id型。

什么是內嵌id型,就是父級引用子級的id,子級作為單獨實體(表)存在。即這里的公司有員工id的引用。

 

實體

內嵌id型一對多實體

 

MongoDB設計

公司表

{
    "id":"45465516654",
    "名稱":"千度科技有限公司",
    "注冊地址":"北京市朝陽區",
    "所有人":"張大大",
    "注冊日期":"2001-9-1",
    "員工":[
        {            "id":"員工id1"        },        {            "id":"員工id2"        }
    ]
}

 

員工表

{
    "id":"員工id",
    "姓名":"alun",
    "入職時間":"2017-8-8",
    "身份證號":"441955876632155502",
    "職位":"架構師",
    "工資":8000,
    "入職年限":1,
    "頭像":"https://www.dr.cn/head/sdfjooc2143.jpg",
    "公資金百分比":5,
    "得獎數":1,
    "體重":"50KG",
    "住址":"廣東省廣州市天河區龍洞",
    "下屬人數":20,
    "年假剩余天數":0,
    "評價級別":10
}

 

在查詢時:

查詢公司下面有的員工信息,兩張表的關聯查詢。首先查詢公司表,然后關聯員工表查詢員工的信息。

查詢單個員工信息時,只需查詢員工表,就能取得員工實體信息。

在修改時:

修改單個員工信息時,修改員工的實體即可。

 

與SQL的對比:

如果是SQL的設計,員工表就多了一個公司表的id做關聯。公司表也不會有員工的ids。

在查詢公司下有哪些員工時,mongodb與關系型SQL各有秋千。因為mongodb首先查詢公司表,得到員工的id后,再關聯查詢。注意這里的關聯查詢員工的主鍵是id,默認加了索引。如果是關系型SQL,那么需要在員工表加入公司表的id做關聯,此種查詢只需通過查詢員工表的公司id即可得出員工的信息。

更深一層說,如果查詢公司名稱是“百度公司”時,mongodb與關系型SQL都需要聯表查詢,鑒於mongodb有id的內嵌並且是索引,理論上來說mongodb快很多。

 

小結                                                                                

使用內嵌id型一對多設計時,前提有這幾點:

    1)一對多的多那方數量要多,最好是幾十個到幾千個不等。

    2)如果需要單獨把內嵌的實體取出。即單獨取出多那方的實體。

這種方案的缺點:

    1)查詢員工屬於哪些公司時,需要跨表查詢。

    2)內嵌方的數量不能過多。

 

3.2.3 場景3,內嵌id+查詢字段型一對多

繼續引用上面公司與員工的場景。

 

如果有個功能,查詢公司下面的員工名字。這個功能是占用查詢率70%以上的話,可以考慮使用內嵌id+查詢字段型。

即在公司內嵌員工id的同時,加上員工的姓名。如下:

{
    "id":"45465516654",
    "名稱":"千度科技有限公司",
    "注冊地址":"北京市朝陽區",
    "所有人":"張大大",
    "注冊日期":"2001-9-1",
    "員工":[
        {            "id":"員工id1",            "姓名":"alun"        },
        {            "id":"員工id2",            "姓名":"vivien"      }
    ]
}

 

在查詢時,不需要管理表,直接查詢公司表即可。查詢效率大大提升。

但是相應地,修改名字的時候需要修改公司表、員工表。需要一次都修改2張表成功。需要原子性的操作。

 

小結                                                                                           

使用內嵌id+查詢字段型一對多設計時,前提有這幾點:

    1)一對多的多那方數量要多,最好是幾十個到幾千個不等。

    2)內嵌方的屬性(字段)不宜過多。

    3)查詢、修改比高。即查詢需求大大大於修改需求。

這種方案的缺點:

    1)修改時需要原子性操作。

    2)文檔內容加大了,即產生了多余字段。如上面的【姓名】。

    3)需要特殊場景需求。

 

3.2.4 場景4,父級引用型一對多

列舉了上面的公司與員工的場景,如果有種需求,多那方是大量的話,那怎么辦呢?

場景:某寶/某東 與商鋪的關系。據不完全統計,某寶商鋪有幾萬到幾十萬,甚至幾百萬不等。

 

上面這么大量的數據,顯然使用內嵌型有些力不從心。因為文檔大小也有限制,如果太大的話,不禁mongodb報錯,還可能導致網絡延遲。因為一次查詢的數據量過大。

所以這里就使用父級引用。什么是父級引用,即子級引用父級id。如商鋪引用某寶id。

 

實體:

父級引用型一對多

 

 

MongoDB設計

某寶/某東表

{
    "id":"hijgio19089popik",
    "名稱":"某寶",
    "注冊地址":"杭州市",
    "所有人":"某某某",
    "注冊日期":"2005-1-1"
}

 

商鋪表

{
    "id":"84948654",
    "名稱":"alun的商鋪",
    "創建時間":"2018-2-1",
    "過期時間":"2019-2-1",
    "是否合法":true,
    "商品品質級別":"高",
    "是否個人商鋪":true,
    "營業執照":"https://yyzz.tb.cn/kg/145/84948654.jpg",
    "持有人身份證號":"441922365587444468",
    "持有人身份證圖片":"https://yyzz.tb.cn/kg/966/441922365587444468.jpg",
    "某寶/某東id":"hijgio19089popik"
}

 

查詢時:

查詢屬於某寶的商鋪時,在商鋪表通過某寶/某東id查出所有的商鋪。幾萬商鋪以上。

修改時:

修改商鋪、修改某東/某寶表的屬性都是單個表修改。

從上面可以看出,一的那方不需要做關聯,只需要多的那方有一的那方的id即可。這種設計完全是套用關系型SQL的用法。很多關系型SQL都這樣使用。所以對比關系型SQL,這種用法在MongoDB來說沒有什么優點。

 

小結                                                                                            

使用父級引用型一對多設計時,前提有這幾點:

    1)一對多的多那方數量很多,上萬以上。

    2)對性能要求不高。

這種方案的缺點:

    1)性能不高。

    2)與關系型SQL相比,沒有什么優點。

 

3.2.5 場景5,內嵌id+父級引用混合型一對多

我們再針對某寶分析吧,比如一個商鋪,有很多個商品。

場景:商鋪與商品的關系。一個商鋪對應很多個商品(幾百、幾千、上萬個不等)。

想象一下以下場景:

    1)商品的屬性很多。

    2)一個商鋪下面的商品很多。

    3)查詢時,查詢到商鋪按類別查詢、按銷量查詢的需求。

    4)查詢時,查詢商鋪的所有商品。此查詢率比較高,進入商鋪就需要用到。

    5)修改時,單獨修改商品的規格、價格等等。

    6)修改商品信息較少發生。

綜上場景,使用3.2.1內嵌設計型不符合我們的需求。

3.2.2 內嵌id型,好像可以滿足我們的現在需求。但是目前有個需求的查詢率比較高,就是第4點。再加上商鋪是幾百到上萬不等,所以需要再考慮。

3.2.3 內嵌id+查詢字段型也不符合我們的需求,因為通過商鋪查詢某些商品屬性的場景不存在。

3.2.4 父級引用好像蠻合適的。但是效率有點低,暫時不考慮。不如使用關系型SQL。

總結一下上面的不同方案,好像內嵌id型與父級引用都蠻適合的,但是各有優劣,那么我們怎么辦呢?混合試試?

 

實體:

內嵌id 父級引用混合型一對多

 

如果有上面的實體,想象一下自己怎么設計呢?對,使用內嵌id型一對多。

什么是內嵌id型一對多呢?父級只引用子級對象的id。我們看看下面的mongoDB的JSON格式。

 

首先是商鋪表:

{
    "id":"3455-2dp4x-xderd0",
    "名稱":"alun的商鋪",
    "類型":"葯材專賣店",
    "所有人":"alun",
    "創建日期":"2018-9-1",
    "商品":[
        {            "id":"商品id1"        },
        {            "id":"商品id2"        }
    ]
}

 

然后是商品表:

{
    "id":"商品id1",
    "名稱":"枸杞",
    "類型":"中葯補血",
    "是否上架":"是",
    "創建日期":"2018-9-15",
    "商品詳情":"農民枸杞直銷,價格原廠,不經過加工。",
    "商品編碼":"878866554234",
    "規格":"1kg",
    "運費":0,
    "重量":11,
    "標簽":"中葯材枸杞",
    "銷售量":0,
    "銷售價":20,
    "原價":40,
    "商鋪id":"3455-2dp4x-xderd0"
}

 

查詢時:

查詢到商鋪按類別查詢、按銷量查詢的需求。查詢商品表即可。

查詢商鋪的所有商品。查詢商鋪表的商品,再通過商品id查詢所有的商品表。所以要關聯查詢。

修改時:

如果要修改商品與商鋪的關系,需要原子性刪除,兩張表做操作。但是這種操作在需求上來很少見。上面需求也列明了,所以這方面性能暫時不考慮。

小結                                                                  

使用內嵌id+父級引用混合型一對多設計時,前提有這幾點:

    1)一對多的多方數量中等,最好是幾百到上萬即可。

    2)一對多的多方數量存在嚴重的不確定性。

這種方案的缺點:

    1)混合模式,折中方案,沒有太大的優勢。

 

總結

      通過上面的內容,可以了解到,在項目設計時,mongodb需要考慮的方面實在太多了,相對的關系型SQL考慮的方面少很多。所以在設計mongodb之前,需要考慮數據的關系,使用的場景,對應業務的需求量。比如查詢與修改率、是否內嵌。

      好的設計,都離不開對業務的整體、細致的理解。mongodb讓我們對數據更加細致的理解,更加面向對象。

      所以,盡可能的去嘗試mongodb吧,大家有什么分享或者不同意見的請不含蓄地提出,共同學習才是最好的成長!

 

備注:書面表達能力還待提高。

 

 

 

 

如果文章對你有用,可以通過以下方式支持作者。

image

 

 

可以關注本人的公眾號,多年經驗的原創文章共享給大家。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM