mongo數據庫的表設計原則:一


Mongodb 數據庫表格設計原則

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)混合模式,折中方案,沒有太大的優勢。


免責聲明!

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



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