1. 程式人生 > >MongoDB資料庫設計法則

MongoDB資料庫設計法則

Part 1

By William Zola, Lead Technical Support Engineer at MongoDB

“我有豐富的sql使用經驗,但是我是個MongoDB的初學者。我應該如何在MongoDB中針對一對多關係進行建模?”這是我被問及最多的問題之一。

我沒法簡單的給出答案,因為這有很多方案去實現。接下來我會教導你如何針對一對多進行建模。

這個話題有很多內容需要討論,我會用三個部分進行說明。在第一部分,我會討論針對一對多關係建模的三種基礎方案。在第二部分我將會覆蓋更多高階內容,包括反正規化化和雙向引用。在最後一部分,我將會回顧各種選擇,並給出做決定時需要考慮的因素。

很多初學者認為在MongoDB中針對一對多建模唯一的方案就是在父文件中內嵌一個數組子文件,但是這是不準確的。因為你可以在MongoDB內嵌一個文件不代表你就必須這麼做。

當你設計一個MongoDB資料庫結構,你需要先問自己一個在使用關係型資料庫時不會考慮的問題:這個關係中集合的大小是什麼樣的規模?你需要意識到一對很少,一對許多,一對非常多,這些細微的區別。不同的情況下你的建模也將不同。

Basics: Modeling One-to-Few : 一對很少

針對個人需要儲存多個地址進行建模的場景下使用內嵌文件是很合適,可以在person文件中嵌入addresses陣列文件:


這種設計具有內嵌文件設計中所有的優缺點。最主要的優點就是不需要單獨執行一條語句去獲取內嵌的內容。最主要的缺點是你無法把這些內嵌文件當做單獨的實體去訪問。

例如,如果你是在對一個任務跟蹤系統進行建模,每個使用者將會被分配若干個任務。內嵌這些任務到使用者文件在遇到“查詢昨天所有的任務”這樣的問題時將會非常困難。我會在下一篇文章針對這個用例提供一些適當的設計。

Basics: One-to-Many:一對許多

以產品零件訂貨系統為例。每個商品有數百個可替換的零件,但是不會超過數千個。這個用例很適合使用間接引用---將零件的objectid作為陣列存放在商品文件中(在這個例子中的ObjectID我使用更加易讀的2位元組,現實世界中他們可能是由12個位元組組成的)。

每個零件都將有他們自己的文件物件


每個產品的文件物件中parts陣列中將會存放多個零件的ObjectID :


在獲取特定產品中所有零件,需要一個應用層級別的join

為了能快速的執行查詢,必須確保products.catalog_number有索引。當然由於零件中parts._id一定是有索引的,所以這也會很高效。

這種引用的方式是對內嵌優缺點的補充。每個零件是個單獨的文件,可以很容易的獨立去搜索和更新他們。需要一條單獨的語句去獲取零件的具體內容是使用這種建模方式需要考慮的一個問題(請仔細思考這個問題,在第二章反反正規化化中,我們還會討論這個問題)

這種建模方式中的零件部分可以被多個產品使用,所以在多對多時不需要一張單獨的連線表。

Basics: One-to-Squillions一對非常多

我們用一個收集各種機器日誌的例子來討論一對非常多的問題。由於每個mongodb的文件有16M的大小限制,所以即使你是儲存ObjectID也是不夠的。我們可以使用很經典的處理方法“父級引用”---用一個文件儲存主機,在每個日誌文件中儲存這個主機的ObjectID。


以下是個和第二中方案稍微不同的應用級別的join用來查詢一臺主機最近5000條的日誌資訊


所以,即使這種簡單的討論也有能察覺出mongobd的建模和關係模型建模的不同之處。你必須要注意一下兩個因素:
1.一對多中的多是否需要一個單獨的實體。
2.這個關係中集合的規模是一對很少,很多,還是非常多。

基於以上因素來決定採取一下三種建模的方式
1.一對很少且不需要單獨訪問內嵌內容的情況下可以使用內嵌多的一方。
2.一對多且多的一端內容因為各種理由需要單獨存在的情況下可以通過陣列的方式引用多的一方的。
3.一對非常多的情況下,請將一的那端引用嵌入進多的一端物件中。

下一次我們將會看到如何使用雙向關係和反正規化化去提升以上三種基本方案的效能。

Part 2

在上一篇文章中我介紹了三種基本的設計方案:內嵌,子引用,父引用,同時說明了在選擇方案時需要考慮的兩個關鍵因素。

一對多中的多是否需要一個單獨的實體。

這個關係中集合的規模是一對很少,很多,還是非常多。

在掌握了以上基礎技術後,我將會介紹更為高階的主題:雙向關聯和反正規化化。

雙向關聯

如果你想讓你的設計更酷,你可以讓引用的“one”端和“many”端同時儲存對方的引用。

以上一篇文章討論過的任務跟蹤系統為例。有person和task兩個集合,one-to-n的關係是從person端到task端。在需要獲取person所有的task這個場景下需要在person這個物件中儲存有task的id陣列,如下面程式碼所示。


在某些場景中這個應用需要顯示任務的列表(例如顯示一個多人協作專案中所有的任務),為了能夠快速的獲取某個使用者負責的專案可以在task物件中嵌入附加的person引用關係。


這個方案具有所有的一對多方案的優缺點,但是通過新增附加的引用關係。在task文件物件中新增額外的“owner”引用可以很快的找到某個task的所有者,但是如果想將一個task分配給其他person就需要更新引用中的person和task這兩個物件(熟悉關係資料庫的童鞋會發現這樣就沒法保證操作的原子性。當然,這對任務跟蹤系統來說並沒有什麼問題,但是你必須考慮你的用例是否能夠容忍)

在一對多關係中應用反正規化

在你的設計中加入反正規化,可以使你避免應用層級別的join讀取,當然,代價是這也會讓你在更新是需要操作更多資料。下面我會舉個例子來進行說明

反正規化Many -< One

以產品和零件為例,你可以在parts陣列中冗餘儲存零件的名字。以下是沒有加入反正規化設計的結構。


反正規化化意味著你不需要執行一個應用層級別的join去顯示一個產品所有的零件名字,當然如果你同時還需要其他零件資訊那這個應用層的join是避免不了的。


在使得獲取零件名字簡單的同時,執行一個應用層級別的join會和之前的程式碼有些區別,具體如下:


反正規化化在節省你讀的代價的同時會帶來更新的代價:如果你將零件的名字冗餘到產品的文件物件中,那麼你想更改某個零件的名字你就必須同時更新所有包含這個零件的產品物件。

在一個讀比寫頻率高的多的系統裡,反正規化是有使用的意義的。如果你很經常的需要高效的讀取冗餘的資料,但是幾乎不去變更他d話,那麼付出更新上的代價還是值得的。更新的頻率越高,這種設計方案的帶來的好處越少。

例如:假設零件的名字變化的頻率很低,但是零件的庫存變化很頻繁,那麼你可以冗餘零件的名字到產品物件中,但是別冗餘零件的庫存。

需要注意的是,一旦你冗餘了一個欄位,那麼對於這個欄位的更新將不在是原子的。和上面雙向引用的例子一樣,如果你在零件物件中更新了零件的名字,那麼更新產品物件中儲存的名字欄位前將會存在短時間的不一致。

反正規化One -< Many

你也可以冗餘one端的資料到many端:


如果你冗餘產品的名字到零件表中,那麼一旦更新產品的名字就必須更新所有和這個產品有關的零件,這比起只更新一個產品物件來說代價明顯更大。這種情況下,更應該慎重的考慮讀寫頻率。

在一對很多的關係中應用反正規化

在日誌系統這個一對許多的例子中也可以應用反正規化化的技術。你可以將one端(主機物件)冗餘到日誌物件中,或者反之。

下面的例子將主機中的IP地址冗餘到日誌物件中。


如果想獲取最近某個ip地址的日誌資訊就變的很簡單,只需要一條語句而不是之前的兩條就能完成。


事實上,如果one端只有少量的資訊儲存,你甚至可以全部冗餘儲存到多端上,合併兩個物件。


另一方面,也可以冗餘資料到one端。比如說你想在主機文件中儲存最近的1000條日誌,可以使用MongoDB 2.4中新加入的$eache/$slice功能來保證list有序而且只儲存1000條。

日誌物件儲存在logmsg集合中,同時冗餘到hosts物件中。這樣即使hosts物件中超過1000條的資料也不會導致日誌物件丟失。


通過在查詢中使用投影引數 (類似{_id:1})的方式在不需要使用logmsgs陣列的情況下避免獲取整個mongodb物件,1000個日誌資訊帶來的網路開銷是很大的。

在一對多的情況下,需要慎重的考慮讀和更新的頻率。冗餘日誌資訊到主機文件物件中只有在日誌物件幾乎不會發生更新的情況下才是個好的決定。

總結

在這篇文章裡,我介紹了對三種基礎方案:內嵌文件,子引用,父引用的補充選擇。

使用雙向引用來優化你的資料庫架構,前提是你能接受無法原子更新的代價。

可以在引用關係中冗餘資料到one端或者N端。

在決定是否採用反正規化化時需要考慮下面的因素:

你將無法對冗餘的資料進行原子更新。

只有讀寫比較高的情況下才應該採取反正規化化的設計。

下次,我將會告訴你在面對這些方案時該如何抉擇。


Part 3

這篇文章是系列的最後一篇。在第一篇文章裡,我介紹了三種針對“一對多 ”關係建模的基礎方案。在第二篇文章中,我介紹了對基礎方案的擴充套件:雙向關聯和反正規化化。

反正規化可以讓你避免一些應用層級別的join,但是這也會讓更新變的更復雜,開銷更大。不過冗餘那些讀取頻率遠遠大於更新頻率的欄位還是值得的。

如果你還沒有讀過前兩篇文章,歡迎一覽。

讓我們回顧下這些方案

你可以採取內嵌,或者建立one端或者N端的引用,也可以三者兼而有之。

你可以在one端或者N端冗餘多個欄位

下面這些是你需要謹記的:

1、優先考慮內嵌,除非有什麼迫不得已的原因。

2、需要單獨訪問一個物件,那這個物件就不適合被內嵌到其他物件中。

3、陣列不應該無限制增長。如果many端有數百個文件物件就不要去內嵌他們可以採用引用ObjectID的方案;如果有數千個文件物件,那麼就不要內嵌ObjectID的陣列。該採取哪些方案取決於陣列的大小。

4、不要害怕應用層級別的join:如果索引建的正確並且通過投影條件(第二章提及)限制返回的結果,那麼應用層級別的join並不會比關係資料庫中join開銷大多少。

5、在進行反正規化設計時請先確認讀寫比。一個幾乎不更改只是讀取的欄位才適合冗餘到其他物件中。

6、在MongoDB中如何對你的資料建模,取決於你的應用程式如何去訪問它們。資料的結構要去適應你的程式的讀寫場景。

設計指南

當你在MongoDB中對“一對多”關係進行建模,你有很多的方案可供選擇,所以你必須很謹慎的去考慮資料的結構。下面這些問題是你必須認真思考的:

關係中集合的規模有多大:是一對很少,很多,還是非常多?

對於一對多中”多“的那一端,是否需要單獨的訪問它們,還是說它們只會在父物件的上下文中被訪問。

被冗餘的欄位的讀寫的比例是多少?

資料建模設計指南

在一對很少的情況下,你可以在父文件中內嵌陣列。

在一對很多或者需要單獨訪問“N”端的資料時,你可以採用陣列引用ObjectID的方式。如果可以加速你的訪問也可以在“N”端使用父引用。

在一對非常多的情況下,可以在“N”端使用父引用。

如果你打算在你的設計中引入冗餘等反正規化設計,那麼你必須確保那些冗餘的資料讀取的頻率遠遠大於更新的頻率。而且你也不需要很強的一致性。因為反正規化化的設計會讓你在更新冗餘欄位時付出一定的代價(更慢,非原子化)