摘要:反范式化與范式化相反將每個(gè)文檔所需的數(shù)據(jù)都嵌入在文檔內(nèi)部。決定何時(shí)采用范式化何時(shí)采用反范式化時(shí)比較困難的。范式化能夠提高數(shù)據(jù)寫入速度,反范式化能夠提高數(shù)據(jù)讀取速度。
原文地址:http://pwhack.me/post/2014-06-25-1 轉(zhuǎn)載注明出處
本文摘錄自《MongoDB權(quán)威指南》第八章,可以徹底回答以下兩個(gè)問題:
http://segmentfault.com/q/1010000000364944
http://segmentfault.com/q/1010000000364944
數(shù)據(jù)表示的方式有很多種,其中最重要的問題之一就是在多大程度上對(duì)數(shù)據(jù)進(jìn)行范式化。范式化(normalization)是將數(shù)據(jù)分散到多個(gè)不同的集合,不同集合之間可以相互引用數(shù)據(jù)。雖然很多文檔可以引用某一塊數(shù)據(jù),但是這塊數(shù)據(jù)只存儲(chǔ)在一個(gè)集合中。所以,如果要修改這塊數(shù)據(jù),只需修改保存這塊數(shù)據(jù)的那一個(gè)文檔就行了。但是,MongoDB沒有提供連接(join)工具,所以在不同集合之間執(zhí)行連接查詢需要進(jìn)行多次查詢。
反范式化(denormalization)與范式化相反:將每個(gè)文檔所需的數(shù)據(jù)都嵌入在文檔內(nèi)部。每個(gè)文檔都擁有自己的數(shù)據(jù)副本,而不是所有文檔共同引用同一個(gè)數(shù)據(jù)副本。這意味著,如果信息發(fā)生了變化,那么所有相關(guān)文檔都需要進(jìn)行更新,但是在執(zhí)行查詢時(shí),只需要一次查詢,就可以得到所有數(shù)據(jù)。
決定何時(shí)采用范式化何時(shí)采用反范式化時(shí)比較困難的。范式化能夠提高數(shù)據(jù)寫入速度,反范式化能夠提高數(shù)據(jù)讀取速度。需要根據(jù)自己應(yīng)用程序的十幾需要仔細(xì)權(quán)衡。
數(shù)據(jù)表示的例子假設(shè)要保存學(xué)生和課程信息。一種表示方式是使用一個(gè)students集合(每個(gè)學(xué)生是一個(gè)文檔)和一個(gè)classes集合(每門課程是一個(gè)文檔)。然后用第三個(gè)集合studentsClasses保存學(xué)生和課程之間的聯(lián)系。
> db.studentsClasses.findOne({"studentsId": id}); { "_id": ObjectId("..."), "studentId": ObjectId("..."); "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
如果比較熟悉關(guān)系型數(shù)據(jù)庫(kù),可能你之前建國(guó)這種類型的表連接,雖然你的每個(gè)記過文檔中可能只有一個(gè)學(xué)生和一門課程(而不是一個(gè)課程“_id”列表)。將課程放在數(shù)組中,這有點(diǎn)兒MongoDB的風(fēng)格,不過實(shí)際上通常不會(huì)這么保存數(shù)據(jù),因?yàn)橐?jīng)歷很多次查詢才能得到真實(shí)信息。
假設(shè)要找到一個(gè)學(xué)生所選的課程。需要先查找students集合找到學(xué)生信息,然后查詢studentClasses找到課程“_id”,最后再查詢classes集合才能得到想要的信息。為了找出課程信息,需要向服務(wù)器請(qǐng)求三次查詢。很可能你并不想再M(fèi)ongoDB中用這種數(shù)據(jù)組織方式,除非學(xué)生信息和課程信息經(jīng)常發(fā)生變化,而且對(duì)數(shù)據(jù)讀取速度也沒有要求。
如果將課程引用嵌入在學(xué)生文檔中,就可以節(jié)省一次查詢:
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
"classes"字段是一個(gè)數(shù)組,其中保存了John Doe需要上的課程“_id”。需要找出這些課程的信息時(shí),就可以使用這些“_id”查詢classes集合。這個(gè)過程只需要兩次查詢。如果數(shù)據(jù)不需要隨時(shí)訪問也不會(huì)隨時(shí)發(fā)生變化(“隨時(shí)”比“經(jīng)常”要求更高),那么這種數(shù)據(jù)組織方式是非常好的。
如果需要進(jìn)一步優(yōu)化讀取速度,可以將數(shù)據(jù)完全反范式化,將課程信息作為內(nèi)嵌文檔保存到學(xué)生文檔的“classes”字段中,這樣只需要一次查詢就可以得到學(xué)生的課程信息了:
{ "_id": ObjectId("..."), "name": "John Doe" "classes": [ { "class": "Trigonometry", "credites": 3, "room": "204" }, { "class": "Physics", "credites": 3, "room": "159" }, { "class": "Women in Literature", "credites": 3, "room": "14b" }, { "class": "AP European History", "credites": 4, "room": "321" } ] }
上面這種方式的優(yōu)點(diǎn)是只需要一次查詢就可以得到學(xué)生的課程信息,缺點(diǎn)是會(huì)占用更多的存儲(chǔ)空間,而且數(shù)據(jù)同步更困難。例如,如果物理學(xué)的學(xué)分變成了4分(不再是3分),那么選修了物理學(xué)課程的每個(gè)學(xué)生文檔都需要更新,而且不只是更新“Physics”文檔。
最后,也可以混合使用內(nèi)嵌數(shù)據(jù)和引用數(shù)據(jù):創(chuàng)建一個(gè)子文檔數(shù)組用于保存常用信息,需要查詢更詳細(xì)信息時(shí)通過引用找到實(shí)際的文檔:
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ { "_id": ObjectId("..."), "class": "Trigonometry" }, { "_id": ObjectId("..."), "class": "Physics" }, { "_id": ObjectId("..."), "class": "Women in Literature" }, { "_id": ObjectId("..."), "class": "AP European History" } ] }
這種方式也是不錯(cuò)的選擇,因?yàn)閮?nèi)嵌的信息可以隨著需求的變化進(jìn)行修改,如果希望在一個(gè)頁(yè)面中包含更多(或者更少)的信息,就可以將更多(或者更少)的信息放在內(nèi)嵌文檔中。
需要考慮的另一個(gè)重要問題是,信息更新更頻繁還是信息讀取更頻繁?如果這些數(shù)據(jù)會(huì)定期更新,那么范式化是比較好的選擇。如果數(shù)據(jù)變化不頻繁,為了優(yōu)化更新效率兒犧牲讀寫速度就不值得了。
例如,教科書上介紹范式化的一個(gè)例子可能是將用戶和用戶地址保存在不同的集合中。但是,人們幾乎不會(huì)改變住址,所以不應(yīng)該為了這種概率極小的情況(某人改變了住址)而犧牲每一次查詢的效率。在這種情況下,應(yīng)該將地址內(nèi)嵌在用戶文檔中。
如果決定使用內(nèi)嵌文檔,更新文檔時(shí),需要設(shè)置一個(gè)定時(shí)任務(wù)(cron job),以確保所做的每次更新都成功更新了所有文檔。例如,我們?cè)噲D將更新擴(kuò)散到多個(gè)文檔,在更新完成所有文檔之前,服務(wù)器崩潰了。需要能夠檢測(cè)到這種問題,并且重新進(jìn)行未完的更新。
一般來(lái)說,數(shù)據(jù)生成越頻繁,就越不應(yīng)該將這些內(nèi)嵌到其他文檔中。如果內(nèi)嵌字段或者內(nèi)嵌字段數(shù)量時(shí)無(wú)限增長(zhǎng)的,那么應(yīng)該將這些內(nèi)容保存在多帶帶的集合中,使用引用的方式進(jìn)行訪問,而不是內(nèi)嵌到其他文檔中,評(píng)論列表或者活動(dòng)列表等信息應(yīng)該保存在多帶帶的集合中,不應(yīng)該內(nèi)嵌到其他文檔中。
最后,如果某些字段是文檔數(shù)據(jù)的一部分,那么需要將這些字段內(nèi)嵌到文檔中。如果在查詢文檔時(shí)經(jīng)常需要將某個(gè)字段排除,那么這個(gè)字段應(yīng)該放在另外的集合中,而不是內(nèi)嵌在當(dāng)前的文檔中。
更適合內(nèi)嵌 | 更適合引用 |
---|---|
子文檔較小 | 子文檔較大 |
數(shù)據(jù)不會(huì)定期改變 | 數(shù)據(jù)經(jīng)常改變 |
最終數(shù)據(jù)一致即可 | 中間階段的數(shù)據(jù)必須一致 |
文檔數(shù)據(jù)小幅增加 | 文檔數(shù)據(jù)大幅增加 |
數(shù)據(jù)通常需要執(zhí)行二次查詢才能獲得 | 數(shù)據(jù)通常不包含在結(jié)果中 |
快速讀取 | 快速寫入 |
假如我們有一個(gè)用戶集合。下面是一些可能需要的字段,以及它們是否應(yīng)該內(nèi)嵌到用戶文檔中。
用戶首選項(xiàng)(account preferences)用戶首選項(xiàng)只與特定用戶相關(guān),而且很可能需要與用戶文檔內(nèi)的其他用戶信息一起查詢。所以用戶首選項(xiàng)應(yīng)該內(nèi)嵌到用戶文檔中。
最近活動(dòng)(recent activity)這個(gè)字段取決于最近活動(dòng)增長(zhǎng)和變化的頻繁程度。如果這是個(gè)固定長(zhǎng)度的字段(比如最近的10次活動(dòng)),那么應(yīng)該將這個(gè)字段內(nèi)嵌到用戶文檔中。
好友(friends)通常不應(yīng)該將好友信息內(nèi)嵌到用戶文檔中,至少不應(yīng)該將好友信息完全內(nèi)嵌到用戶文檔中。下節(jié)會(huì)介紹社交網(wǎng)絡(luò)應(yīng)用的相關(guān)內(nèi)容。
所有由用戶產(chǎn)生的內(nèi)容不應(yīng)該內(nèi)嵌在用戶文檔中。
基數(shù)一個(gè)集合中包含的對(duì)其他集合的引用數(shù)量叫做基數(shù)(cardinality)。常見的關(guān)系有一對(duì)一、一對(duì)多、多對(duì)多。假如有一個(gè)博客應(yīng)用程序。每篇博客文章(post)都有一個(gè)標(biāo)題(title),這是一個(gè)對(duì)一個(gè)的關(guān)系。每個(gè)作者(author)可以有多篇文章,這是一個(gè)對(duì)多的關(guān)系。每篇文章可以有多個(gè)標(biāo)簽(tag),每個(gè)標(biāo)簽可以在多篇文章中使用,所以這是一個(gè)多對(duì)多的關(guān)系。
在MongoDB中,many(多)可以被分拆為兩個(gè)子分類:many(多)和few(少)。假如,作者和文章之間可能是一對(duì)少的關(guān)系:每個(gè)作者只發(fā)表了為數(shù)不多的幾篇文章。博客文章和標(biāo)簽可能是多對(duì)少的關(guān)系:文章數(shù)量實(shí)際上很可能比標(biāo)簽數(shù)量多。博客文章和評(píng)論之間是一對(duì)多的關(guān)系:每篇文章可以擁有很多條評(píng)論。
只要確定了少與多的關(guān)系,就可以比較容易地在內(nèi)嵌數(shù)據(jù)和引用數(shù)據(jù)之間進(jìn)行權(quán)衡。通常來(lái)說,“少”的關(guān)系使用內(nèi)嵌的方式會(huì)比較好,“多”的關(guān)系使用引用的方式比較好。
好友、粉絲、以及其他的麻煩事情親近朋友,遠(yuǎn)離敵人
很多社交類的應(yīng)用程序都需要鏈接人、內(nèi)容、粉絲、好友,以及其他一些事物。對(duì)于這些高度關(guān)聯(lián)的數(shù)據(jù)使用內(nèi)嵌的形式還是引用的形式不容易權(quán)衡。這一節(jié)會(huì)介紹社交圖譜數(shù)據(jù)相關(guān)的注意事項(xiàng)。通常,關(guān)注、好友或者收藏可以簡(jiǎn)化為一個(gè)發(fā)布、訂閱系統(tǒng):一個(gè)用戶可以訂閱另一個(gè)用戶相關(guān)的通知。這樣,有兩個(gè)基本操作需要比較高效:如何保存訂閱者,如何將一個(gè)事件通知給所有訂閱者。
比較常見的訂閱實(shí)現(xiàn)方式有三種。第一種方式是將內(nèi)容生產(chǎn)者內(nèi)嵌在訂閱者文檔中:
{ "_id": ObjectId("..."), "username": "batman", "email": "batman@waynetech.com", "following": [ ObjectId("..."), ObjectId("...") ] }
現(xiàn)在,對(duì)于一個(gè)給定的用戶文檔,可以使用形如db.activities.find({"user": {"$in": user["following"]}})的方式查詢?cè)撚脩舾信d趣的所有活動(dòng)信息。但是,對(duì)于一條剛剛發(fā)布的活動(dòng)信息,如果要找出對(duì)這條信息感興趣的所有用戶,就不得不查詢所有用戶的“following”字段了。
另一種方式是將訂閱者內(nèi)嵌到生產(chǎn)者文檔中:
{ "_id": ObjectId("..."), "username": "joker", "email": "joker@mailinator.com", "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
當(dāng)這個(gè)生產(chǎn)者新發(fā)布一條信息時(shí),我們立即就可以知道需要給哪些用戶發(fā)布通知。這樣做的缺點(diǎn)時(shí),如果需要找到一個(gè)用戶關(guān)注的用戶列表,就必須查詢整個(gè)用戶集合。這樣方式的優(yōu)缺點(diǎn)與第一種方式的優(yōu)缺點(diǎn)恰好相反。
同時(shí),這兩種方式都存在另一個(gè)問題:它們會(huì)使用戶文檔變得越來(lái)越大,改變也越來(lái)越頻繁。通常,“following”和“followers”字段甚至不需要返回:查詢粉絲列表有多頻繁?如果用戶比較頻繁地關(guān)注某些人或者對(duì)一些人取消關(guān)注,也會(huì)導(dǎo)致大量的碎片。因此,最后的方案對(duì)數(shù)據(jù)進(jìn)一步范式化,將訂閱信息保存在多帶帶的集合中,以避免這些缺點(diǎn)。進(jìn)行這種成都的范式化可能有點(diǎn)兒過了,但是對(duì)于經(jīng)常發(fā)生變化而且不需要與文檔其他字段一起返回的字段,這非常有用。對(duì)“followers”字段做這種范式化使有意義的。
用一個(gè)集合來(lái)保存發(fā)布者和訂閱者的關(guān)系,其中的文檔結(jié)構(gòu)可能如下所示:
{ "_id": ObjectId("..."), //被關(guān)注者的"_id" "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
這樣可以使用戶文檔比較精簡(jiǎn),但是需要額外的查詢才能得到粉絲列表。由于“followers”數(shù)組的大小經(jīng)常會(huì)發(fā)生變化,所以可以在這個(gè)集合上啟用“usePowerOf2Sizes”,以保證users集合盡可能小。如果將followers集合保存在另一個(gè)數(shù)據(jù)庫(kù)中,也可以在不過多影響users集合的前提下對(duì)其進(jìn)行壓縮。
應(yīng)對(duì)威爾惠頓效應(yīng)不管使用什么樣的策略,內(nèi)嵌字段只能在子文檔或者引用數(shù)量不是特別大的情況下有效發(fā)揮作用。對(duì)于比較有名的用戶,可能會(huì)導(dǎo)致用于保存粉絲列表的文檔溢出。對(duì)于這種情況的一種解決方案使在必要時(shí)使用“連續(xù)的”文檔。例如:
> db.users.find({"username": "wil"}) { "_id": ObjectId("..."), "username": "wil", "email": "wil@example.com", "tbc": [ ObjectId("123"), // just for example ObjectId("456") // same as above ], "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("123"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("456"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] }
對(duì)于這種情況,需要在應(yīng)用程序中添加從“tbc”(to be continued)數(shù)組中取數(shù)據(jù)的相關(guān)邏輯。
說點(diǎn)什么No silver bullet.
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/18705.html
摘要:不過這里的時(shí)間指的是系統(tǒng)版本號(hào)死鎖數(shù)據(jù)庫(kù)的解釋現(xiàn)象兩個(gè)或兩個(gè)以上事務(wù)在同一資源相互占用,并請(qǐng)求鎖定對(duì)方占用的資源,從而導(dǎo)致惡性循環(huán)的現(xiàn)象。并發(fā)控制解決問題我在讀數(shù)據(jù),你在刪數(shù)據(jù)的情況鎖分類讀鎖共享鎖,不阻塞寫鎖排他鎖,排除其他寫鎖和讀鎖。 數(shù)據(jù)庫(kù)面試題 showImg(https://s2.ax1x.com/2019/01/11/FXc580.md.jpg); DBS DBMS DB區(qū)...
閱讀 1416·2021-10-11 10:59
閱讀 3114·2019-08-30 15:54
閱讀 2735·2019-08-30 13:19
閱讀 2463·2019-08-30 13:02
閱讀 2376·2019-08-30 10:57
閱讀 3355·2019-08-29 15:40
閱讀 986·2019-08-29 15:39
閱讀 2311·2019-08-29 12:40