使用數據的時候,一個數據項經常和另外的一個或多個數據項產生關係,好比一個「人」對象,有一個名字,可能有多個電話號碼,以及多個子女,等等。 html
在傳統的SQL數據庫中,關係被分爲一個個表(table),在表中,每一個數據項以主鍵(primary key)標識,而一個表的主鍵又做爲另外一個表的外鍵(reference key),在兩個表之間引用。當趕上多對多關係的時候,還須要一個額外的關聯表(reference table),將多對多關係轉化成兩個一對多關係。 python
而在MongoDB中,表示關係有兩種辦法: git
一種是嵌套(embedded),既是將一個文檔包裹一個子文檔; github
而另外一種是引用連接(reference link),使用MongoDB的DBRef對象創建文檔和文檔之間的關係。 mongodb
除此以外,MongoDB的關係比起傳統的SQL表關係更豐富一些,能夠有 1對1 , 1對N , N對1 和 N對N 幾種關係。 數據庫
本文的目的就是探討MongoDB表示關係的方法。 api
首先,讓咱們來看看MongoDB表示數據關係的兩種方式:嵌套和引用連接。 數組
嵌套 函數
每一個MongoDB文檔都由BSON文檔組成,有相似JSON格式同樣的數據類型,其中String、Int、Float稱爲基本類型(或常量),而Hash和Array稱之爲複合類型。 spa
所謂的嵌套,就是說文檔中,利用複合類型,包裹一個多或多個其餘類型的值,這些值稱之爲子文檔。
文檔嵌套的數量和深度沒有限制,但MongoDB目前版本限制一個文檔最大爲16MB。
下面是一個典型的我的檔案文檔(profile),其中有一個 name 常量域,還嵌套了一個朋友(firends)子數組,數組裏面每一個項是一個字典。
>>> huangz {'friends': [{'name': 'peter'}, {'name': 'john'}, {'name': 'marry'}], 'name': 'huangz'}
嵌套的好處是顯而易見的:嵌套文檔維持了數據邏輯上的完整性,能夠將一整項數據做爲一個總體來操縱。
對比在關係式的數據庫中, 爲了設計出符合範式的表,咱們經常要將多個數據項分拆爲幾個表,而後經過外鍵獲取數據。
當你閱讀一個單獨的表的數據時,你一般只看到了其中一部分數據,而其餘的都是外鍵id,就像這樣:
['name': 'huangz', 'friends_reference_id': [12, 26, 30]]
這種數據給人的感受就像打開了一本電話簿,卻發現裏面只有電話號碼,沒有聯繫人姓名,真是太糟糕了阿。
引用連接
比起嵌套,引用連接更接近傳統意義上的(也就是,關係型數據庫術語中的)「引用」,它是兩個文檔之間的一種關係。
引用連接經過DBRef對象創建,DBRef對象儲存瞭如何找到目標文檔的信息,就像現實世界中的門牌號碼同樣(也相似關係型數據庫中的外鍵)。
若是在一個文檔A中,有一個DBRef對象,而這個DBRef對象儲存了關於如何找到文檔B的信息,那麼文檔A就能夠經過解釋這個DBRef對象(稱之爲解引用)來獲取文檔B的數據。
下面是創建一個文檔的引用,以及解引用的過程,此次咱們一樣表示一個一對多的朋友關係,但此次,咱們用連接來創建關係。
>>> # 數據 >>> peter = {'name':'peter'} >>> marry = {'name':'marry'} >>> john = {'name':'john'} >>> >>> # 插入數據 >>> c.test.people.insert([peter, john, marry]) [ObjectId('4e98075224b7d408dc000004'), ObjectId('4e98075224b7d408dc000005'), ObjectId('4e98075224b7d408dc000006')] >>> >>> # 創建huangz文檔,以及指向各個朋友的連接 >>> huangz = {'name': 'huangz', ... 'friends': [ DBRef('people', peter['_id']), ... DBRef('people', john['_id']), ... DBRef('people', marry['_id']) ]} >>> >>> c.test.people.insert(huangz) ObjectId('4e9807d924b7d408dc000007') >>> >>> # 查看huangz文檔 >>> huangz {'_id': ObjectId('4e9807d924b7d408dc000007'), 'friends': [DBRef('people', ObjectId('4e98075224b7d408dc000004')), DBRef('people', ObjectId('4e98075224b7d408dc000005')), DBRef('people', ObjectId('4e98075224b7d408dc000006'))], 'name': 'huangz'} >>> >>> # 對friends中的全部域進行解引用 >>> [ c.test.dereference(friend) for friend in huangz['friends'] ] [{u'_id': ObjectId('4e98075224b7d408dc000004'), u'name': u'peter'}, {u'_id': ObjectId('4e98075224b7d408dc000005'), u'name': u'john'}, {u'_id': ObjectId('4e98075224b7d408dc000006'), u'name': u'marry'}]
OK,雖然有點複雜,但相信聰明的你仍是能夠看出,我創建了三個字典(Dict,或者說,Hash?)文檔,將它們插入people集合。
而後使用 BDRef 函數,將集合名和字典的 id 做爲參數,對每一個字典生成一個 DBRef 對象,而後將三個DBRef對象保存到 friends 域當中。
在最後,我遍歷 huangz 文檔中的 friends 域,將全部 DBRef 對象解引用,獲取了三個相應的子文檔。
-----
附錄
引用和解引用的API:
http://api.mongodb.org/python/current/api/bson/dbref.html#bson.dbref.DBRef
關於DBRef的官方文檔:
http://www.mongodb.org/display/DOCS/Database+References
自動解引用
目前爲止,咱們看過了嵌套和引用連接兩種使用子文檔的方式,可是,慢着,嵌套和引用連接的區別是什麼?
它們最大的區別彷佛在於麻煩程度——嵌套文檔能夠直接使用子文檔中的數據,而引用連接則要先解引用,而後才能獲取子文檔的數據。
若是你也以爲解引用至關麻煩的話,那我有一個好消息要告訴你:pymongo有一個超方便的特性,稱之爲「自動解引用」,就是每當 pymongo 在你文檔中的數據域中發現包含有 DBRef 對象,你忠實的好朋友 pymongo 就會自動幫你進行解引用,你一個手指頭都不用動!
看看使用自動解引用狀況下,以前的 friends 例子:
>>> from pymongo.connection import Connection >>> # 自動解引用所需的函數 >>> from pymongo.son_manipulator import AutoReference, NamespaceInjector >>> >>> # 鏈接數據庫,並選擇 test 數據庫做爲儲存 >>> c = Connection() >>> db = c.test >>> >>> # 魔法發生的地方。。。 >>> db.add_son_manipulator(NamespaceInjector()) >>> db.add_son_manipulator(AutoReference(db)) >>> >>> # 數據 >>> peter = {'name': 'peter'} >>> john = {'name': 'john'} >>> marry = {'name': 'marry'} >>> >>> # 將數據插入數據庫 >>> db.people.insert([peter, john, marry]) [ObjectId('4e980fe824b7d41f52000000'), ObjectId('4e980fe824b7d41f52000001'), ObjectId('4e980fe824b7d41f52000002')] >>> >>> # huangz 文檔 >>> huangz = {'name': 'huangz', ... 'friends': [peter, john, marry]} >>> db.people.insert(huangz) ObjectId('4e98104324b7d41f52000003') >>> >>> # 看一看,文檔裏多了一些奇怪的東西。。。嗯。。。 >>> huangz {'_ns': u'people', '_id': ObjectId('4e98104324b7d41f52000003'), 'friends': [{'_ns': u'people', '_id': ObjectId('4e980fe824b7d41f52000000'), 'name': 'peter'}, {'_ns': u'people', '_id': ObjectId('4e980fe824b7d41f52000001'), 'name': 'john'}, {'_ns': u'people', '_id': ObjectId('4e980fe824b7d41f52000002'), 'name': 'marry'}], 'name': 'huangz'} >>> >>> # 試試提取 friends 域中的數據(注意這裏咱們沒有手動進行解引用) >>> huangz['friends'][0] {'_ns': u'people', '_id': ObjectId('4e980fe824b7d41f52000000'), 'name': 'peter'} >>> >>> # wow,解引用自動完成了!再試試 >>> huangz['friends'][0]['name'] 'peter' >>> >>> # 這裏你可能會疑惑,咱們是否是使用嵌套將數據放進了huangz文檔裏面,而實際上沒有使用引用? >>> # 答案是,不是的,咱們使用了引用,看看 peter 文檔,它有一個id >>> # 證實 peter 文檔是一個獨立文檔,而不是一個嵌入文檔: >>> peter {'_ns': u'people', '_id': ObjectId('4e980fe824b7d41f52000000'), 'name': 'peter'} >>> >>> # 若是你仍是不信,咱們能夠試試修改 peter 文檔,而後再保存 >>> # 看看 huangz 文檔裏面的 peter,是否會變化 >>> peter['name'] = 'peter Liu' >>> db.people.save(peter) ObjectId('4e980fe824b7d41f52000000') >>> >>> # 哈,huangz 文檔裏面的 peter 也發生了變化,證實咱們使用的 >>> # 是引用而不是嵌套,嘿,我但是童叟無欺、信譽良好的阿。 >>> huangz['friends'][0]['name'] 'peter Liu'
在上面的代碼中,咱們增長了兩個函數,爲 db 綁定了兩個構造器,從那以後,解引用就魔術般自動完成了。
好,這樣一來,使用嵌套文檔和使用引用連接彷佛就沒有什麼不一樣了。
嘿嘿,如今我要問你一個嚴肅的問題了:使用嵌套文檔和連接文檔,究竟有什麼不一樣?
請你儘量地獨立思考一下,而後再繼續閱讀下一節。
-----
附錄
自動解引用的例子:
嵌套 v.s. 引用連接
終於來到了謎底揭開的時刻了!若是答案你本身沒想出來,也不用心灰意冷,畢竟我就是來告訴你這個的,若是你本身把答案想出來了,這反而沒有意思了不是麼?
關於嵌套和引用連接的真正區別是——它們的名字是不一樣的!
好吧,你必定已經知道這個了,其實,關於嵌套和引用連接的真正卻別是——它們的英文名字是不一樣的!
好吧,你必定也已經知道了這個,那咱們來講點嚴肅的。
嵌套和引用連接的區別在於,它們對待其餘文檔(子文檔)的方式不一樣,因而它們獲取獲取子文檔數據的方式也不一樣。
先來看嵌套,當咱們將一個子文檔,好比 friends 數組(它自己也是一個單獨的文檔)嵌套於 huangz 文檔時, friends 文檔其實和 huangz 文檔是一體的,其實它們是一個文檔,它們的數據儲存在一塊兒。
而對於引用連接,當你取出一個包含引用連接的文檔時,文檔裏面的 DBRef 對象不會被解開,你取出的是一個包含 DBRef對象的子文檔。而那些 DBRef 對象所指向的文檔,它們是獨立的一個個文檔,從某個角度來看,和這個文檔沒有任何關係。(你對自動解引用感到疑惑麼,我立刻就會說到這個,請稍等。)
所以,每次,當你對DBRef執行解引用的時候,你須要一次額外的查詢。
啊哈,這就是嵌套和引用連接之間的區別了!
咱們用兩個文檔,一個嵌套,一個引用連接,解釋查找它們的過程,來講明它們之間的區別。
先說嵌套文檔,這個文檔以下(跟咱們以前看的同樣):
>>> peter = {'name': 'peter'} >>> john = {'name': 'john'} >>> marry = {'name': 'marry'} >>> >>> huangz = {'name': 'huangz', 'friends': [peter, john, marry]} >>> >>> people.insert(huangz) ObjectId('4e98e30b24b7d40a53000002') >>> >>> huangz {'_id': ObjectId('4e98e30b24b7d40a53000002'), 'friends': [{'name': 'peter'}, {'name': 'john'}, {'name': 'marry'}], 'name': 'huangz'}
咱們能夠用 huangz 文檔的 id 做爲條件,查找 huangz 文檔:
>>> people.find_one({'_id': huangz['_id']}) {u'_id': ObjectId('4e98e30b24b7d40a53000002'), u'friends': [{u'name': u'peter'}, {u'name': u'john'}, {u'name': u'marry'}], u'name': u'huangz'}
當執行上面的語句時,MongoDB會根據 id ,查找文檔,它在數據庫內部苦苦找尋,而後,終於找到了你所需的文檔。
因而MongoDB就將文檔返回給你,由於整個 huangz 文檔——包括 name 數據域和 friends 數據域都放在一塊兒,因此,執行這個文檔只須要一次數據庫查詢,嗯,就是這樣。
接着再來看看帶有引用連接的文檔是怎麼處理的:
>>> peter = {'name': 'peter'} >>> john = {'name': 'john'} >>> marry = {'name': 'marry'} >>> >>> people.insert([peter, john, marry]) [ObjectId('4e98e41b24b7d40a53000003'), ObjectId('4e98e41b24b7d40a53000004'), ObjectId('4e98e41b24b7d40a53000005')] >>> >>> huangz = {'name': 'huangz', ... 'friends': [ DBRef('people', peter['_id']), ... DBRef('people', john['_id']), ... DBRef('people', marry['_id']) ] } >>> people.insert(huangz) ObjectId('4e98e4da24b7d40a53000006')
當我查找 huangz 文檔時,會發生些什麼?
>>> people.find_one({'_id': huangz['_id']}) {u'_id': ObjectId('4e98e4da24b7d40a53000006'), u'friends': [DBRef(u'people', ObjectId('4e98e41b24b7d40a53000003')), DBRef(u'people', ObjectId('4e98e41b24b7d40a53000004')), DBRef(u'people', ObjectId('4e98e41b24b7d40a53000005'))], u'name': u'huangz'}
你的老朋友MongoDB(如今大家應該很熟悉了)接到你的查詢任務,而後開始在數據庫再次苦苦尋找,而後,終於找到你須要的文檔,因而,MongoDB返回這個文檔。
尋找這個文檔也只須要一次數據庫查詢(暫時和嵌套文件同樣),可是,請注意,這個文檔和以前的嵌套文檔不一樣,它包含的不是你所須要的數據,而是 DBRef 對象,三個 DBRef 對象分別儲存瞭如何去找到 peter、jhon 和 marry 文檔的信息,但它們自己還不是你所找的數據(還記得那個只有號碼沒有姓名的電話簿嗎?)。
「嘿,咱們有 dereference 函數,能夠用它進行解引用」,我猜你在這麼想——你說得對,咱們能夠對 huangz 文檔裏的 friends 數據域中的三個 DBRef 文檔進行解引用:
>>> for friend in huangz['friends']: ... db.dereference(friend) ... {u'_id': ObjectId('4e98e41b24b7d40a53000003'), u'name': u'peter'} {u'_id': ObjectId('4e98e41b24b7d40a53000004'), u'name': u'john'} {u'_id': ObjectId('4e98e41b24b7d40a53000005'), u'name': u'marry'}
而後,慢着,在解引用的過程當中,發生了什麼呢?
首先,MongoDB接到第一個 DBRef 文件,上面指名咱們所找的文檔的 id 和 集合,因而乎,MongoDB就根據這個地址,去幫咱們搜索 id 爲 '4e98e41b24b7d40a53000003' 的文檔,它苦苦找尋,終於發現了 peter 文檔,因而,它將 peter 文檔返回給你,這個過程須要一個數據庫查詢。
而後,MongoDB 接到第二個 DBRef 文件,上面又有一個新地址,因而 MongoDB 根據這個地址又開始查找,一陣搜索以後,它找到了 john 文檔,並返回給你,這個過程又須要一個數據庫查詢。
再次,MongoDB 接到第三個 DBRef 文件,上面又有一個新地址,不辭勞苦的 MongoDB 再一次開始工做,此次,它找到並將 marry 文檔返回給你,這個過程又須要一個數據庫查詢。
最終,你使用引用連接的文檔得到了和嵌套文檔同樣的數據,但引用連接的文檔使用了 4 次數據庫查詢(一次 huangz 文檔,peter、john 和 marry各一次),而嵌套文檔只使用 1 次數據庫查詢。
噢噢噢,你有種突然明白了一些什麼的感受,是嗎?
結論
OK,這時候,咱們差很少該下結論說,爲了最大效率地使用數據庫,你應該儘量地使用嵌套而不是引用連接來設計你的MongoDB文檔,由於每次解引用都會帶來一次額外的搜索,它的速度比使用嵌套的文檔慢的多(取決於解引用的次數)。
可是還有幾個微妙的問題,在下結論以前,須要好好地解釋一下:
1. 自動解引用和手動解引用的效率是否不一樣?
答案是,自動解引用和手動解引用的效率是同樣的,它們使用相同的數據庫查詢次數,
當你啓用了自動解引用以後,pymongo每次發現你取出的文檔裏面帶有 DBRef 對象時,它就幫你調用 deference 函數,得到 DBRef 所指向的文檔的數據,可是,它不會也不可能將 4 次數據庫查詢換成 1 次數據庫查詢。
2. 使用引用連接,對效率的影響有多大?
答:一個包含引用連接文檔所需的數據庫查詢次數正比於解引用的次數 + 1(獲取這個文檔自己),而所需的查詢次數越多,就越慢。
像上面 huangz 文檔的例子,引用連接版本理論上須要的時間就是嵌套版本的 3 倍。
3. 彷佛嵌套文檔真不錯,但若是我有一個 weibo 文檔(一個微博文檔),它包括 name 和 message,兩個數據域。
message 就是儲存微博的數據域,而 name 則是用戶名字,message 數據域確定會比 name 數據域大不少,那麼,這個 weibo 文檔取出的速度,會受到 message 數據域的大小影響嗎,name 數據域呢?
答:這的確是使用嵌套文檔須要當心的地方,由於整個文檔可能很大,若是你不當心使用查詢語句,確保只取出必須的數據的話,你可能會浪費一些時間讀取沒必要要的字段。
好比,以微薄爲例子,假如你使用下面這個查詢語句,全部屬於 huangz 的微薄都會被取出來,這可能不是你想要的結果(好比文檔裏有 10萬 條數據,但你只想取出前 100 條,那樣的話,其餘 9900 條數據都被浪費了)。
data = weibo.find_one({'name': 'huangz'})
而換成如下數據,你能夠只取出最新100條數據,不浪費任何資源:
data = weibo.find_one({'name': 'huangz'}, {'message': {'$slice': -100}})
(你可能發現這個查詢語句仍是有一些問題,嗯,的確是這樣,由於它取出來的數據是倒轉過來的,也便是,最新發的微薄在最低下,咱們之後再來解決這個問題。)
若是你在看別人的微薄,那時候你一條信息都不須要取出來,你只須要其餘我的信息,好比 name,因此,你應該使用如下語句:
data = weibo.find_one({'name': 'huangz'}, {'message: 0})
實際上,使用引用鏈接的文檔也有這個問題,並且比嵌套文檔的狀況更嚴重(由於它更慢)。
因此,不管什麼狀況下,請確保你使用了正確的查詢語句,確保只取出所需的數據。
4.若是我先用一個查詢取出一個嵌套文檔的某一部分,再用另外一個查詢取出這個嵌套文檔的另外一部分,這比使用一個引用連接的文檔快嗎?
答:不,由於它們都須要兩次數據庫查詢,效果相差無幾。
---
附錄
子域查詢的語法 http://www.mongodb.org/display/DOCS/Retrieving+a+Subset+of+Fields
時間關係。。。
好了,這時候,你應該對「使用嵌套文檔加上合理的查詢,能夠得到最高效率」這一結論深信不疑了。
嘿嘿,但我有個壞消息要告訴你,就是並非全部文檔關係都能使用嵌套文檔來設計,有時候,必須用到引用連接。
不過,由於時間關係,以及爲了點擊量的可持續發展,我決定把這個問題和剩下的內容(1對一、1對N等關係設計和示例)留到下一篇博客再寫。
嗯,就這樣,咱們下篇博文再見,bye bye。
---
附錄
MongoDB schema design http://www.mongodb.org/display/DOCS/Schema+Design#SchemaDesign-EmbeddingandLinking