《Windows Azure Platform 系列文章目錄》html
咱們在使用NoSQL的時候,如Azure Cosmos DB,能夠很是快速的查詢非結構化,或半結構化的數據。咱們須要花一些時間,研究Cosmos DB的數據建模,來保證查詢性能和可擴展性,同事下降成本。web
閱讀完這篇文章後,咱們將學會:數據庫
1.什麼是數據建模,爲何咱們要關注數據建模數組
2.如何在Azure Cosmos DB進行數據建模,與傳統關係型數據庫有什麼不一樣服務器
3.如何在非關係型數據庫中,保存關係型數據dom
4.何時執行嵌入(embed)數據,何時執行鏈接(link)數據post
嵌入(embed)數據性能
當咱們開始在Cosmos DB進行數據建模的時候,嘗試對咱們的數據實體(Entity)視爲自包含(Self-contained items)並保存在JSON文件中優化
爲了比較,讓咱們首先看一下如何在關係型數據庫中進行數據建模。下面的案例將介紹咱們在關係型數據庫中,如何保存用戶信息this
當咱們使用關係型數據庫的時候,通常都須要將數據規範化(Normalize)。規劃範數據通常都會引入數據實體,好比一我的,咱們能夠將用戶信息分解爲不一樣的屬性信息。
在上面的例子中,一我的有多個聯繫人,也有多個地址。聯繫人的詳細信息能夠進一步進行分解並提取經常使用字段。咱們也能夠用一樣的方法,對地址進行分解,好比地址的類型能夠是家庭地址,或者是公司地址。
規範化數據的指導方法是避免存儲冗餘的數據,而且應用數據。在上面的示例中,咱們若是要讀取一我的的全部聯繫人的詳細信息和地址,咱們須要使用JOIN方法,查詢到所須要的數據:
SELECT p.FirstName, p.LastName, a.City, cd.Detail FROM Person p JOIN ContactDetail cd ON cd.PersonId = p.Id JOIN ContactDetailType on cdt ON cdt.Id = cd.TypeId JOIN Address a ON a.PersonId = p.Id
若是咱們更新一我的的聯繫人信息和地址,須要跨多張表執行更新操做
如今咱們看看如何在Azure Cosmos DB使用自包含(Self-contained items)實體
{ "id": "1", "firstName": "Thomas", "lastName": "Andersen", "addresses": [ { "line1": "100 Some Street", "line2": "Unit 1", "city": "Seattle", "state": "WA", "zip": 98012 } ], "contactDetails": [ {"email": "thomas@andersen.com"}, {"phone": "+1 555 555-5555", "extension": 5555} ] }
上面咱們使用了非規範化(denormalized)來保存人的記錄,咱們將與人相關的全部信息,好比聯繫人的詳細信息和地址信息,嵌入到單個JSON文檔中,從而對人的記錄進行了非規範化。另外,由於咱們不侷限於固定的Schema,咱們能夠靈活的使用不一樣類型的聯繫人信息
從Cosmos DB中讀取一條記錄,如今只須要單個讀取操做。更新人的記錄,包括聯繫人信息和地址信息,也只須要一次寫入的操做。
經過使用非規範化(denormalized)保存數據,相比傳統的關係型數據庫,咱們的應用程序的讀取和更新的操做能夠減小。
何時使用嵌入(embed)數據
咱們通常在如下狀況下,使用嵌入數據:
1.數據實體之間有包含(contained)關係
2.數據實體之間有1對多的關係
3.嵌入(embed)數據不常常變化
4.嵌入的數據不會無限增加
5.嵌入的數據是頻繁集中查詢的
一般非規範化(denormzlized)數據模型具備更好的讀取性能
何時不使用嵌入(embed)數據
雖然Azure Cosmos DB中的經驗法則是對全部內容進行非規範化,並將全部數據嵌入到單個項目中,但這可能會致使某些狀況:
咱們觀察下面的JSON:
{ "id": "1", "name": "What's new in the coolest Cloud", "summary": "A blog post by someone real famous", "comments": [ {"id": 1, "author": "anon", "comment": "something useful, I'm sure"}, {"id": 2, "author": "bob", "comment": "wisdom from the interwebs"}, … {"id": 100001, "author": "jane", "comment": "and on we go ..."}, … {"id": 1000000001, "author": "angry", "comment": "blah angry blah angry"}, … {"id": ∞ + 1, "author": "bored", "comment": "oh man, will this ever end?"}, ] }
若是咱們對一個博客系統進行建模,上面的例子就是採用嵌入(embed)數據方法,存儲評論(comments)數據。上面例子的問題是評論的數據是沒有限制的,這意味着任何一個發佈的POST的內容,都有無限多個評論數據。這會讓Cosmos DB的JSON文件變的無限大,可能會產生問題。
隨着Cosmos DB的數據尺寸變的愈來愈大,讀取數據和更新數據可能會產生影響。
在這種狀況下,咱們最好能夠考慮採用如下的數據建模。
Post item: { "id": "1", "name": "What's new in the coolest Cloud", "summary": "A blog post by someone real famous", "recentComments": [ {"id": 1, "author": "anon", "comment": "something useful, I'm sure"}, {"id": 2, "author": "bob", "comment": "wisdom from the interwebs"}, {"id": 3, "author": "jane", "comment": "....."} ] } Comment items: { "postId": "1" "comments": [ {"id": 4, "author": "anon", "comment": "more goodness"}, {"id": 5, "author": "bob", "comment": "tails from the field"}, ... {"id": 99, "author": "angry", "comment": "blah angry blah angry"} ] }, { "postId": "1" "comments": [ {"id": 100, "author": "anon", "comment": "yet more"}, ... {"id": 199, "author": "bored", "comment": "will this ever end?"} ] }
上面的數據模型中,在一個Container中,包含了最新三個評論,且評論具備固定的屬性。
其餘的評論信息是保存在單獨的Container中,每一個container保存100條數據。Batch的大小設置爲100,是由於咱們假設應用程序容許用戶一次加載100條評論數據
另外的場景中,嵌入(embed)數據並非一個好的主意,好比嵌入的數據須要常常跨項目使用,且常常發生變化
咱們能夠參考下面的JSON內容:
{ "id": "1", "firstName": "Thomas", "lastName": "Andersen", "holdings": [ { "numberHeld": 100, "stock": { "symbol": "zaza", "open": 1, "high": 2, "low": 0.5 } }, { "numberHeld": 50, "stock": { "symbol": "xcxc", "open": 89, "high": 93.24, "low": 88.87 } } ] }
這個場景是我的投資的股票信息。咱們選擇將股票信息嵌入到每一個投資組合文檔中。在一個相關數據頻繁變化的環境中,如股票交易應用程序,嵌入頻繁變化的數據意味着您每次交易股票時都會不斷更新每一個投資組合文檔。
股票zaza可能在一天內被交易數百次,成千上萬的用戶能夠在他們的投資組合中擁有zaza。 使用上述數據模型,咱們天天必須屢次更新數千個投資組合文檔,致使系統沒法很好地擴展。
引用數據 (Referencing data)
所以,在大多數狀況下使用嵌入(embed)數據能夠很好的處理業務場景,可是很明顯在某些場景下,非規範化數據將致使更多的問題而得不償失。咱們如今應該怎麼辦?
關係型數據庫不是在數據數據實體之間建立關係的惟一選擇。在Document Database中,咱們能夠在一個Document中建立對另一個Document的引用。咱們並非說使用 Azure Cosmos DB能夠更好的適應關係型數據庫,或者其餘Document Database。咱們僅僅說明在Azure Cosmos DB中也可使用簡單的關係,而且頗有用。
在下面的JSON文檔中,咱們選擇以前的股票投資組合的示例,可是咱們採用了引用數據的關係,而不是嵌入數據(embed)。在這種狀況下,當一天中股票信息發生頻繁變化的時候,咱們只須要更新股票的Document。
Person document: { "id": "1", "firstName": "Thomas", "lastName": "Andersen", "holdings": [ { "numberHeld": 100, "stockId": 1}, { "numberHeld": 50, "stockId": 2} ] } Stock documents: { "id": "1", "symbol": "zaza", "open": 1, "high": 2, "low": 0.5, "vol": 11970000, "mkt-cap": 42000000, "pe": 5.89 }, { "id": "2", "symbol": "xcxc", "open": 89, "high": 93.24, "low": 88.87, "vol": 2970200, "mkt-cap": 1005000, "pe": 75.82 }
不過, 這種方法的一個直接缺點是, 若是您的應用程序須要顯示有關在顯示一我的的投資組合時持有的每隻股票的信息;在這種狀況下, 您須要屢次訪問數據庫以加載每一個庫存文檔的信息。在這裏, 咱們決定提升寫入操做的效率, 這些操做在一天中頻繁發生, 但反過來又影響了對此特定系統的性能影響較小的讀取操做。
規範化數據模型可能須要屢次訪問服務器
外鍵在哪裏?
在Document Database中並不存在約束,外鍵或其餘相似概念。因此在Document Database中,任何Document之間的關係都是「弱連接」的關係,而且Document Database不會驗證這些關係。若是想要確保文檔要引用的數據實際存在,則需在應用程序中進行此驗證,或經過使用 Azure Cosmos DB 上的服務器端觸發器或存儲過程來驗證。
何時使用引用?
咱們通常在如下狀況下,使用引用數據:
1.一對多的關係
2.多對多的關係
3.數據須要頻繁更改
4.使用數據可能沒有限制
一般規範化可以提供更好的編寫性能。
將關係存儲在哪裏?
關係的增加將有助於肯定用於存儲引用的文檔。
讓咱們看看下面的對出版商和書籍進行建模的 JSON 代碼。
Publisher document: { "id": "mspress", "name": "Microsoft Press", "books": [ 1, 2, 3, ..., 100, ..., 1000] } Book documents: {"id": "1", "name": "Azure Cosmos DB 101" } {"id": "2", "name": "Azure Cosmos DB for RDBMS Users" } {"id": "3", "name": "Taking over the world one JSON doc at a time" } ... {"id": "100", "name": "Learn about Azure Cosmos DB" } ... {"id": "1000", "name": "Deep Dive into Azure Cosmos DB" }
若是每一個出版商的書籍數量較少且增加有限,那麼在出版商文檔中存儲書籍引用可能頗有用。 可是,若是每一個出版商的書籍數量沒有限制,那麼此數據模型將產生可變、不斷增加的數組,相似於上面示例中的出版商文檔。
稍微作些更改就會使模型仍顯示相同的數據,但能夠避免產生較大的可變集合。
Publisher document: { "id": "mspress", "name": "Microsoft Press" } Book documents: {"id": "1","name": "Azure Cosmos DB 101", "pub-id": "mspress"} {"id": "2","name": "Azure Cosmos DB for RDBMS Users", "pub-id": "mspress"} {"id": "3","name": "Taking over the world one JSON doc at a time"} ... {"id": "100","name": "Learn about Azure Cosmos DB", "pub-id": "mspress"} ... {"id": "1000","name": "Deep Dive into Azure Cosmos DB", "pub-id": "mspress"}
在上面的示例中,咱們刪除了出版商文檔中的無限制集合, 只在每一個書籍文檔中引用出版商。
如何處理多對多關係(Many: Many)進行數據建模
在關係型數據庫中,多對多關係一般使用錶鏈接來實現,錶鏈接就是將其餘表的記錄鏈接在一塊兒
可能想要使用文檔複製相同內容,並生成相似如下示例的數據模型。
Author documents: {"id": "a1", "name": "Thomas Andersen" } {"id": "a2", "name": "William Wakefield" } Book documents: {"id": "b1", "name": "Azure Cosmos DB 101" } {"id": "b2", "name": "Azure Cosmos DB for RDBMS Users" } {"id": "b3", "name": "Taking over the world one JSON doc at a time" } {"id": "b4", "name": "Learn about Azure Cosmos DB" } {"id": "b5", "name": "Deep Dive into Azure Cosmos DB" } Joining documents: {"authorId": "a1", "bookId": "b1" } {"authorId": "a2", "bookId": "b1" } {"authorId": "a1", "bookId": "b2" } {"authorId": "a1", "bookId": "b3" }
此模型可行。 可是,加載一個做者及其書籍或加載一個書籍及其做者,將始終要求對數據庫執行至少兩次查詢。 一次是對聯接文檔的查詢,另外一個查詢用來獲取聯接的實際文檔。
若是聯接表只是將兩個數據片斷聯接在一塊兒,那麼爲何不將該表徹底刪除? 請考慮如下代碼。
Author documents: {"id": "a1", "name": "Thomas Andersen", "books": ["b1, "b2", "b3"]} {"id": "a2", "name": "William Wakefield", "books": ["b1", "b4"]} Book documents: {"id": "b1", "name": "Azure Cosmos DB 101", "authors": ["a1", "a2"]} {"id": "b2", "name": "Azure Cosmos DB for RDBMS Users", "authors": ["a1"]} {"id": "b3", "name": "Learn about Azure Cosmos DB", "authors": ["a1"]} {"id": "b4", "name": "Deep Dive into Azure Cosmos DB", "authors": ["a2"]}
如今,若是我有做者的姓名,我能夠當即知道他們所寫的哪些書,相反若是我有一個書籍文檔加載我能夠知道做者的 Id。 這能夠省去對聯接表的中間查詢,從而減小了應用程序須要往返訪問服務器的次數。
混合數據建模
如今咱們已經看了嵌入數據(或非規範化)和引用數據(規範化)的示例,正如咱們看到的每種方法都有其優勢和缺點。
不須要始終只使用其中一種方法,能夠大膽地將這兩種方法結合使用。
根據應用程序的特定使用模式和工做負載,可能在一些狀況下結合使用嵌入式數據和引用數據是有意義的,可產生具備更少的服務器往返訪問次數的更簡單的應用程序邏輯,同時仍保持較好的性能級別。
請考慮如下 JSON。
Author documents: { "id": "a1", "firstName": "Thomas", "lastName": "Andersen", "countOfBooks": 3, "books": ["b1", "b2", "b3"], "images": [ {"thumbnail": "https://....png"} {"profile": "https://....png"} {"large": "https://....png"} ] }, { "id": "a2", "firstName": "William", "lastName": "Wakefield", "countOfBooks": 1, "books": ["b1"], "images": [ {"thumbnail": "https://....png"} ] } Book documents: { "id": "b1", "name": "Azure Cosmos DB 101", "authors": [ {"id": "a1", "name": "Thomas Andersen", "thumbnailUrl": "https://....png"}, {"id": "a2", "name": "William Wakefield", "thumbnailUrl": "https://....png"} ] }, { "id": "b2", "name": "Azure Cosmos DB for RDBMS Users", "authors": [ {"id": "a1", "name": "Thomas Andersen", "thumbnailUrl": "https://....png"}, ] }
此處咱們(主要)遵循了嵌入式模型,在頂層文檔中嵌入其餘實體的數據,但同時引用了其餘數據。
若是查看書籍文檔(Book)中的做者數組,會看到一些有趣的字段。 在Document Book中,咱們有一個authors:id字段,經過id字段咱們能夠查到找Document Author中的信息,這是一個標準的規範化模型。可是咱們在Document Book中,還包含了name和thumbnailUrl字段。咱們能夠經過Document Book中的authors:id字段,查找到Document Author中的其餘屬性。由於在這個應用程序中,咱們須要在每一本書中顯示做者的名稱和做者的縮略圖,因此使用非規範化(denormalizing)的方式,把做者的名稱和做者的縮略圖,預先保存到Document Book中,減小額外的傳輸和IO開銷。
固然,若是做者的名稱更改,或者他們想要更新本身的照片,咱們將須要轉並更新他們曾經發布,但咱們的應用程序,基於做者不常常更改其名稱的假設每本書,這是一個可接受的設計。
在示例中預先計算的聚合值可在讀取操做上節省高昂的處理成本。 在本例中,做者文檔中嵌入的一些數據爲在運行時計算的數據。 每當出版了一本新書,就會建立一個書籍文檔而且將 countOfBooks 字段設置爲基於特定做者的現有書籍文檔數的計算值。 這種優化對於讀取頻繁的系統來講是有益的,爲了優化讀取,咱們能夠對寫入操做執行更多計算。
由於 Azure Cosmos DB 支持多文檔事務,因此構建一個具備預先計算字段的模型是可能的。許多 NoSQL 存儲沒法跨文檔執行事務,正是由於該限制,因此提倡諸如「始終嵌入全部數據」的設計決策。 在 Azure Cosmos DB 中,可使用服務器端觸發器或存儲過程在一個 ACID 事務中插入書籍和更新做者信息等。 如今無需將全部數據嵌入一個文檔,只需確保數據保持一致性。
區分不一樣的文檔類型
在一些場景中,咱們可能須要在一個Collection中,保存不一樣類型的文檔。這一般是這種狀況,若是但願多個相關的文檔中保存在相同的分區。 例如,能夠將這兩個叢書和同一集合中的書評和分區經過bookId
。 在這種狀況下,你一般想要添加到文檔中使用字段,用於標識其類型以區分它們。
Book documents: { "id": "b1", "name": "Azure Cosmos DB 101", "bookId": "b1", "type": "book" } Review documents: { "id": "r1", "content": "This book is awesome", "bookId": "b1", "type": "review" }, { "id": "r2", "content": "Best book ever!", "bookId": "b1", "type": "review" }