在圖數(shù)據(jù)庫里,有時候會碰到一個看起來要做出一些取舍的地方,那就是:很多元素都有的,且重復值很多的一個屬性,是應該將其作為頂點的屬性來存儲,還是當做一個頂點,然后用邊來將元素指向該頂點來表示一個屬性?例如以下的例子:
在我們的圖中需要存儲用戶注冊時的phone段和ip段。這兩個屬性是所有的用戶頂點都需要的屬性,而且這兩個屬性的重復度比較高,也就是很多很多頂點都有相同的phone段或者ip段。此時我們有兩種不同的思路來解決這個問題:
思路1:將phone和ip當做用戶頂點的屬性。根據(jù)我們需要的讀取模式,給phone和ip創(chuàng)建索引來加快查詢效率。
思路2:phone和ip不作為屬性,而是將不同的phone和ip段當做頂點保存在圖數(shù)據(jù)庫中,用戶的ip和phone的值通過邊指向不同的代表phone的頂點和ip的頂點,通過這種方式來實現(xiàn)保存其屬性。
表面看起來這兩種方式都可以實現(xiàn)功能,實際上他們之間還是有很多不同的。我們需要分析其各自不同的利弊,從而在不同的情形下選用不同的模型來解決問題。
用屬性存儲的方式的優(yōu)點:
這種思路更直觀一些,也更符合我們一般對頂點和屬性的理解。
對與頂點屬性本身的查詢效率更高一些,因為不涉及頂點的traversal。如果使用頂點的方式存儲,則涉及到邊的向外遍歷。尤其是如果我們要一次獲取多個屬性時,這種方式直接在頂點內就可以獲取,而第二種方式則需要遍歷n條邊來獲取n個屬性。
存儲需要的空間小一些,不需要在storage backend中使用額外的記錄來存儲這些保存屬性的點以及指向這些代表屬性的點的邊。
圖的schema會簡單一些。不需要額外定義多個vertex label和edge label。
在導入頂點時速度會快一些,尤其是在開啟了bulk loading模式時。因為這種方式在添加一個用戶頂點時,只需要添加一個頂點和其對應的屬性即可。而第二種方式里,我們除了需要添加用戶頂點本身,還要在添加用戶頂點之前先把屬性頂點添加到庫里(而在數(shù)據(jù)準備階段,還需要將這些屬性的數(shù)據(jù)單獨剝離出來,并去掉其重復值),并在添加用戶頂點時從庫中查詢已經添加的屬性頂點,并添加指向這些頂點的邊。這會導致圖的準備階段多浪費大量的時間。
用頂點存儲的方式的優(yōu)點:
以上的屬性存儲的方式的優(yōu)點也恰恰是頂點存儲的方式的缺點。這種方式并不直觀,對頂點屬性值的查詢效率也不高,而且會增加額外的記錄來保存點和邊。另外會導致schema變得復雜,也會在導入數(shù)據(jù)時額外浪費時間花在屬性頂點的查詢上。
但這種方式也有其優(yōu)點,那就是將屬性的由點及面變得簡單,尤其是在一個分析中涉及多個由點及面的情形時,這種方式的優(yōu)點就能展現(xiàn)出來。而這也常常是圖數(shù)據(jù)庫需要做的東西,尤其在涉及社群分析時。
我們這里的由點及面代表的含義是:我們需要找出所有的跟某個或者某幾個頂點的某些屬性相同的全部頂點。
以以下的要求為例:當我們需要找到跟張三和李四使用同一個電話段或者ip段的用戶使用的phone和ip的分組情況。
如果我們的圖的schema是基于第一種情況存儲的,這就變成了一個比較棘手的問題。我們需要首先找到張三和李四的電話作為phoneSet,然后找到張三和李四的ip作為ipSet,然后查詢電話和ip與集合中的值相同的記錄。
這種需求,如果放在普通的數(shù)據(jù)庫中,實際上就是根據(jù)屬性的join操作。這種看似簡單的操作在圖數(shù)據(jù)庫中要實現(xiàn)確面臨比較大的困難,原因在于:圖數(shù)據(jù)庫中解決連接的操作是通過邊來實現(xiàn)的,且沒有別的解決方式。換句話說,圖數(shù)據(jù)庫中,相同屬性之間的點是沒有東西能夠將其快速連接起來的。
這也就是第二種方式最大的意義所在。以該題為例,我們通過gremlin可以很容易的實現(xiàn)(假設張三的頂點是v3,李四頂點是v4):
g.V(v3, v4).out("ip", "phone").in("ip", "phone").values().groupCount()
總結:如果屬性重復度很低,或者屬性不涉及連接操作,則使用屬性存儲是更好也更直觀的方式。但如果涉及屬性的連接操作,而且屬性的重復度很高,則可以考慮用點來存儲屬性。