導語
圖數據庫該如何操作和查詢呢?我們知道關系型數據庫用的是SQL( Query ),它也是數據庫領域第一個國際標準,在大數據庫和NoSQL類型數據庫廣泛發展之前的唯一的一個數據庫查詢語言國際標準。
本文中,我們會介紹圖查詢語言GQL(Graph Query )的基礎概念,以及GQL與SQL之間的差異。
01、用進化的視角看數據庫和查詢語言的演進
如果讀者對SQL語言的演進有所了解的話,就知道是它直接推動了關系型數據庫的發展。此外,互聯網的崛起還催生了NoSQL的誕生和崛起,其中重要的一個因素是關系型數據庫無法很好地應對數據處理速度、數據建模靈活性的訴求。NoSQL數據庫一般被分為以下5類,每一類都有其各自的特性:
鍵值: 性能和簡易性
寬列: 體量與性能
文檔:數據多樣性
圖:深數據+快數據
時序:(可選的)時序,IOT數據、時序優先性能
下圖中,SQL被認為是最先進的數據處理與查詢語言,并代表了一大批人的認知,如果更深入地去探究SQL的演化歷史,就能夠幫助我們有一個更全局化的概念了。
圖:數據庫查詢語言的進化(一家之言)
SQL的出現已將近半個世紀了,并且迭代了很多版本(平均每3至4年就一次大迭代),其中最知名的非SQL的92/99莫屬,例如92版本中在FROM語句中增加了子查詢功能,在99 年版本中增加了CTE功能等,這些都極大地增加了關系型數據庫的靈活性。然而,關系型數據庫始終存在一個“弱點”,那就是對于遞歸型數據結構的支持。
所謂遞歸數據結構,指的是有向關系圖的功能實現。令人感到諷刺的是,關系型數據庫的名字雖然包含了關系,但它在設計伊始就很難支持關聯關系的查詢。為了實現關聯查詢,關系型數據庫不得不依賴表連接操作,而每一次表連接都意味著潛在的表掃描操作,以及隨之而來的性能上的指數級下降和SQL語句、代碼復雜度的直線上升。相信這也是很多程序員的夢魘吧。
表連接操作的性能損耗是直接源自于關系型數據庫的基礎設計思想:
數據正則化(Data )
固定化的、預先設定的表模式(Fixed/ )
如果我們看一下NoSQL中的核心理念,在數據建模中突出了數據去正則化。
所謂數據正則化,指的是用空間換取時間(犧牲空間來換取更高的性能!)在NoSQL(也包括等,例如典型的3、5、7份拷貝的理念)中,數據經常被以多份拷貝的方式存儲 ,而這樣做的好處在于數據可以以近鄰計算資源的方式被處理。這種理念和SQL中的只存一份正則化設計思路是截然相反的。后者或許可以節省一些存儲空間,但是對于復雜的SQL操作而言,帶來的是性能的損耗。
預先定義數據的表模式的理念是SQL與NoSQL的另一大差異。對于初次接觸這一概念的讀者而言,理解這個點會有些復雜,從下面這個角度去理解或許更直觀:
在關系型數據庫中,系統管理員(DBA),需要先定義表的結構(),然后才會加載第一行數據進入數據庫,他不可能動態的更改表的結構。這種僵化性對于固定模式、一成不變的數據結構和業務需求而言或許不是什么大問題。但是,讓我們想象一下,如果數據模式可以自我調整,并能根據流入的數據動態調整,這就給了我們極大的靈活性。
對于強SQL背景的人而言,這是很難被想象的。但是,如果我們暫時拋棄掉我們僵化的、限制性的思維,取而代之以一種成長性的思維方式 ——我們所要達成的目標是一種“-Free或”的數據模式,也就是無需預先設定數據模式,數據之間的關聯性不需要預先定義和了解,隨著數據的流入,它們會自然地形成某種關聯關系 ——而數據庫所需要做的是對應著這些數據“因地制宜”地來處理如何查詢與計算。
在過去幾十年中,數據庫程序員已經被訓練的一定要先了解數據模型,不論它是關系型表結構還是實體E-R模式圖。了解數據模型當然有它的優點,然而,這也讓開發流程變得更加復雜和緩慢。如果讀者們還記得上一次你參與的交鑰匙解決方案的開發周期有多長?一個季度、半年、一年還是更久?在一個有8000張表的數據庫中,沒有任何一個DBA可以完全掌握所有表之間的關聯關系。這個時候,我們更愿意把這套脆弱的系統比作一個定時炸彈,而你的所有業務都綁定在其上!關于無模式(-Free),筆者并不想解釋文檔型數據庫或者寬列據庫,盡管它們都多少有一些和圖數據庫相似的設計理念。在下文中會用一些具體的圖數據庫上的例子來幫助讀者理解無模式意味著什么。
02、圖語言(GQL)的大道至簡之法
在圖數據庫中,邏輯上只有兩類基礎的數據類型:
頂點(Nodes 或)
邊(Edges)
一個頂點具有它自己的ID和屬性(標簽、類別及其它屬性)。
邊也類似,除了它通常是由兩個頂點的順序決定的(所謂有向圖的概念指的是每條邊由一個初始頂點對應一個終止頂點,再加上其它屬性所構成,例如邊的方向、標簽、權重等)。
此外,圖數據庫并不需要任何預先定義的模式或表結構。這種極度簡化的理念恰恰和人類如何思考以及存儲信息有著很大的相似性 —— 我們通常并不在腦海中設定表結構,我們是隨機應變的!
現在,讓我們看一看一些真實世界場景中的圖數據庫實現,例如下圖中的一個典型的圖數據集中的頂點的屬性定義,它包含了最初的幾個字段的定義,例如desc, level, name, type,但是也存在一些動態生成、擴增的字段,例如#cc, #pr, #等等。如果比照著關系型數據庫而言,整個表的結構是動態可調整的。
圖:圖數據集中的頂點屬性(動態屬性)
注意上圖中的name和type字段的屬性為類型,它可以最大化兼容廣譜的數據類型,進而提供最大化的靈活性。頂點之間如何產生關聯也無需被預先定義,這樣所形成的關聯網絡也是靈活的。
細心的讀者一定會問到,這種靈活性怎么來實現和保證性能優化呢?常識的做法是通過存儲與計算分層來實現,例如為了實現極佳的計算性能,數據可以動態地加載進入內存計算(LTE vs. UFE = Load-to- vs. -from-),當然內存計算只是一部分,支持并發計算的數據結構也是必不可少的。
鍵值庫可以被看作是前-SQL(Pre-SQL在這里表達的是一種相對于SQL而言更原始的特型)的非關系型架構庫,圖數據庫則可被看作是后SQL(Post-SQL在這里表達了一種相對而言的先進性)時代的,真正意義上支持遞歸式數據結構的數據庫。
今天的不少NoSQL數據庫都試圖通過兼容SQL來獲得認可,但是在筆者看來,SQL的設計理念是極度表限定的,所謂“表限定”(table-),指的是它的整個理念都是限定在二維世界中的,當要進行表連接操作時,就好比要去進入到三維或更高維的空間進行操作,而這也是非常低效和反直覺的,這是基于關系型數據模式—SQL本身的低維屬性決定的。
圖數據庫天然是高維的,除非它的實現是基于關系型數據庫或列數據庫實現的,那么本質上這種非原生圖的設計依然是低維驅動的,它的效率又怎么可能會高呢?!
在圖上面的操作天然是屬于遞歸式的,例如廣度優先搜索或深度優先搜索。當然,僅僅從語言的兼容性而言,圖上面一樣也可以支持SQL類操作來保持向關系型用戶群的習慣兼容,就像Spark SQL或CQL一樣,無論它的意義到底有多大或多長久。
03、 GQL (嬴圖)圖語言
下面,讓我們來看一些通過用Ultpa GQL( Graph Query , 圖數據庫查詢語言)實現的圖查詢功能實例。同時,請仔細思考用SQL 或是其它NoSQL 數據庫將如何才能完成同樣的任務?
任務1:從某個頂點出發,找到它的第1到第K層(跳)的所有鄰居并返回。
Ultpa GQL 是與 Graph 高并發實時圖數據庫匹配的查詢語言,又名嬴圖查詢語言。除了明顯的性能優勢外,Ultpa GQL的另外一個重要特點是高易用性,容易掌握,并有貼近自然語言的易讀性。可以通過 、 CLI或 SDK/API的接口調用,只需要1行 GQL即可實現上面的查詢。
圖:通過()操作對聯通子圖進行遍歷
spread().src(123).depth(6).spread_type(“BFS”).limit(4000);
上面的語句簡單易懂,基本上不需要太多解釋,調用()函數,從頂點123出發,搜索深度為6層,以BFS的方式進行搜索,限定返回最多4000個頂點(以及關聯的邊)。
在上圖中,紅色的小點就是起始頂點,通過以上語句操作的全部返回的頂點和邊所形成的子圖直接顯示在 Graph的WEB界面上了。
事實上,()這個操作相當于允許從任何頂點出發找到它的聯通子圖——或者說它的鄰居網絡的形態可以被直接計算出來,并通過可視化界面直觀地展示出來。用這種方式也可以看出生成的聯通子圖中的頂點和邊所構成的熱點、聚集區域等圖上的空間特征,而并不需要傳統數據庫中的 E-R模型圖。
任務2:給定的多個頂點,自動組網(形成一張頂點間相互聯通的網絡)。
本查詢相對于習慣使用傳統數據庫的讀者來說或許就顯得過于復雜了,用SQL也許無法實現這個組網功能。
但是,對于人的大腦而言,這是個很天然的訴求——當你想在張三、李四、王五和趙六之間組成一張關聯關系的網絡的時,你已經開始在腦海里繪制下面這張圖了(見下圖)。
圖:在 中自動生成的網絡(子圖)
很顯然, GQL傾向于繼續使用1行代碼來實現這個“不可能”的操作:
autoNet().addSrcs(12,21,30,40,123).depth(4).limit(5)
( )就是我們調用的主要函數,它的名字已經非常直白了——自組網操作。你只需要提供一組頂點的ID信息,組網搜索的深度(4層=4跳),以及任意兩個頂點間的路徑數量限制(5)。下面,我們來從純數學的角度來分析一下這個組網操作的計算復雜度:
可能返回路徑數量:C(5, 2) * 5 = (5 * 4 / 2) * 5 = 50 條;
預估圖上計算復雜度:50 * (E/V)4= 50 * 256 = 12800。
注
假設圖中的(邊數/頂點數)比例為4(平均值),也就是E/V=4,搜索深度為4的時候每條路徑需要平均計算256(44)次。
這個查詢在現實世界應用中的意義非同凡響。
例如執法機關會根據電話公司的通話記錄來跟蹤多名嫌疑人的通話所組成的深度網絡的特征來判斷是否有其它嫌疑人關聯其中,犯罪集團是否存在某種異動,或者任意個數的嫌疑人構成的犯罪組織間的微妙的聯動關系等。
在傳統大數據技術框架上,這種多節點的組網操作極為復雜,甚至是沒有可能完成的任務。
原因是因為計算復雜度太高 ,對于計算資源的需求太大,在短時間內沒有可能完成,或者是以T+7(亦或T+15、T+30)的方式實現,等到結果出來的時候,嫌疑人早已逃之夭夭或者罪案已發生良久了。
假設有1000個嫌疑人需要參與組網,他們之間形成的網絡的路徑至少有50萬條(1,000 * 999 / 2)。如果查詢路徑深度為 6層,如上所述,這個計算復雜度是20億次(假設E/V=4,實際上E/V可能>=10,那么計算次數可能達到50萬億次)。基于Spark架構的計算平臺可能需要數天來完成運算。
而利用 Graph,該操作是以實時到近實時(T+0)的方式完成的,我們在不同的數據集上做過性能評測,的性能至少是Spark框架的幾百倍到數千倍——Spark系統需要1天完成的計算, 僅需數秒、數分鐘!當與罪犯斗爭的時候,每一秒都很寶貴。
圖:基于的實時大規模的組網操作
對于實時高并發圖數據庫,性能肯定是“第一等公民”,但是這并沒有讓我們把語言的簡潔、直觀、易懂性當做次等公民。絕大多數人會發現 GQL是如此的簡單,掌握了最基本的語法規則后,通常閱讀操作手冊幾分鐘到30分鐘內,就可以開始寫出你自己的 GQL查詢語句了。
GQL 借鑒并采用了鎖鏈式查詢(chain-query)的語言風格,對于熟悉文檔型數據庫的讀者而言,上手 GQL 就更加簡單了。例如,一個簡單的鏈式路徑(點到點)查詢語句,看下圖:
圖:鏈式圖查詢語言之路徑查詢
這個例子查詢兩個頂點間深度為5度的路徑,限定返回5條路徑,并且返回匹配的屬性“name”(通常是頂點或邊的名稱屬性)。
我們再來看一個稍微復雜一點的例子——模板查詢,當然,它所完成的功能也更加的強大。例如下面的例子中t()代表調用模板查詢,t(a)表達的是設定當前模板別名為a,從頂點12開始,經過一條邊抵達到屬性age值為20的頂點b(別名),返回這個模板所匹配的結果a和抵達頂點b的名字。
和傳統SQL類似的地方是可以對任何過濾條件設置別名,不同之處在于當異構結果a和b.name一同被返回的時候,a表達的是整個模板搜索所對應的路徑結果的集合,而b.name則是一組頂點的屬性的數組集合,如下面兩張圖所示,這種異構靈活性是SQL不具備的。
圖:鏈式圖查詢語言之模板化路徑查詢
圖:模板路徑查詢中返回的異構結果集
下面,我們再用一個例子來說明在圖查詢中使用簡單的查詢語言實現深度的、遞歸式的查詢:
t(a1).n(n1{age:20}).e(e1{rank:{$bt:[20,30]}})[3:7].n(n2).limit(50).return(a1, n1, e1, n2._id, n2.name)
這個語句中,從年齡=20歲的頂點(可能有多個)出發,進行深度為3至7層的路徑搜索查詢抵達某些頂點,并且路徑中每條邊的權重介于20至30之間,找到50條路徑,并返一系列異構的數據(模板匹配的路徑本身、起始頂點、邊、終止定點的兩個屬性)。
這種靈活度在SQL當中,如果不通過書寫大量的封裝代碼是很難實現的,而且這種搜索深度也是令關系型數據庫望而卻步的—通常會發生因內存或系統資源耗盡而導致數據庫出現SEG-FAULT。
任務3:數學統計類型的查詢,例如count( ),sum( ), min( ), ( )等。
例1:這個例子對于SQL編程愛好者而言一點都不陌生——統計一家公司員工的工資總和。
t(p).n(12).le({type:"works_for"}).n(c{type: "human"}).return(sum(c.salary))
在 GQL中也是一句話即實現:
從公司頂點12出發找到所有工作于(邊關系)本公司的員工,別名為c返回他們全部工資之和
在一張小表中,這個操作在SQL語境下同樣毫無壓力,但是在一張大表中(千萬或億萬行),或許這個SQL操作就會因為表掃描而變得緩慢了。
而在數據庫中因為采用相鄰哈希+近鄰存儲的存儲邏輯及并發邏輯優化,這種面向一步抵達的鄰居頂點的數學統計操作幾乎不會受到數據集大小的影響,進而可以讓任務執行時間基本恒定。
例2:統計該公司的員工都來自于哪幾個省:
t(p).n(12).e({type:"works_for"}).n(c{type:"employee"}).return(collect(c.province))
上面的兩個例子是來說明通過 GQL的方式可以實現傳統關系型SQL查詢所能實現的功能。同樣,返回結果也可以以關系型數據庫查詢結果所常用的表單、表格的方式來呈現,例如下面兩張圖所示:
圖:在 中以表格的方式展示結果列表
在上圖中,khop( )操作返回的是從初始頂點出發經過depth( ) 限定的深度搜索后返回的第K層的鄰居的集合,使用( )可選定需要具體返回的屬性。
下圖中展示的是類似的操作在 CLI中返回的結果示例。注意該圖中的時間有兩個維度,引擎時間和全部時間,其中引擎時間是內存圖計算引擎的運算耗費時間,而全部時間還包括一些持久化存儲層的數據轉換的時間。
圖: 在-CLI中以表單的方式返回結果集
任務4:強大的基于模板的全文搜索。
如果一個數據庫系統中不能支持全文搜索,那么我們很難能稱其為完整的數據庫。在圖數據庫支持全文搜索并不是一個全新的事情,例如老牌的圖數據庫Neo4j中通過集成 的全文搜索框架,讓用戶可以通過語句來對頂點(及其屬性)進行全文本搜索。
但是,這種集成開源框架的方式存在一個嚴重的副作用,就是性能預期與實際(查詢)操作中的落差——圖查詢中關注的往往是多層、深度的路徑或K-鄰查詢,而全文搜索匹配僅僅是這類查詢的第一步,試想,當系統集成了外部開源框架后,多套子系統間就存在頻繁的交互和網絡時延,這種查詢的效率可想而知。另一個原因是開源的框架可能存在一些不可預知的一些問題,在生產環境中一旦暴露,修復起來非常困難,這個或許可被看作是開源的一個重大弊端吧。
在 GQL 中完成面向頂點的全文搜索,只需要下面這句簡單的查詢語句:
find().nodes(~name:"Sequoia*").limit(100).select(name,intro)
這句返回的是找到100個包含“”字樣的頂點,并返回它們的name和intro屬性。這個查詢非常類似于傳統數據庫中的面向某張表的列信息查詢。同樣地,也可以針對邊來進行查詢,例如下面:
find().edges(~name:"Love*").limit(200).select(*)
找到圖中所有的邊上的name屬性中存在“愛情”字樣的關系。
當然,如果我們的全文檢索只是停留在點、邊查詢,那么這就略顯單薄了。在真實的商用化的圖數據庫應用場景中,我們更可能用一種基于模板的模糊匹配全文查詢。
例如,模糊的搜索從“紅杉***”出發到“招銀*”的一張關聯關系網絡,網絡中的路徑搜索深度不超過5層,返回20條路徑所構成的子圖。注意:這個搜索從模糊匹配頂點出發,到達模糊匹配的另外一套頂點。
t().n(~name: "紅杉*").e()[:5].n(~name: "招銀*").limit(20).select(name)
如果不用上面這句簡單得不能再簡單的 GQL,你能想象如何用其它SQL或NoSQL語言來實現嗎?
假設我們在一個工商數據集之上,在天眼查、企查查做類似的查詢,你要先找到名字中包含有“紅杉”或“招銀”字樣的公司,然后再分別對每一家公司的投資關系進行梳理,需要查清楚每家被投公司的合作、競爭、董監高等關系,然后再慢慢梳理出來是否能在5步之內關聯上名稱中包含“紅杉”字樣的一家公司和包含“招銀”字樣的另一家公司。這個操作絕對是讓人瘋狂的,你可能需要花費數天的時間來完成,或者能夠通過寫代碼調用API的方式來“智能化”的實現。無論如何,你很難在下面兩件事情上擊敗 GQL:
效率和時延( and ):即實時性
準確率和直觀度( and ):直觀、易讀、易懂
圖:實時地基于模板查詢的全文搜索( )
在上圖中,這個看起來簡單而又實際上非常復雜的查詢操作僅僅耗時50ms!這種復雜查詢的效率是前所未有的。如果讀者知道有任何其它數據庫系統可以在更短的時間內完成同樣的操作,歡迎聯系筆者深度交流。
一門先進的(數據庫)查詢語言的優美感,不是通過它到底有多復雜sql 數據庫關系圖,而是通過它有多簡潔來體現的。它應該具備這樣的一些通性:
易學、易懂(Easy to Learn,Easy to )
高性能( Fast)本質上取決于底層的數據庫引擎
系統的底層復雜性不應該暴露到語言接口層面( -Off)
特別是最后一點,如果讀者對于SQL或、 或中復雜的嵌套邏輯心有余悸,你會更理解下面的這個比喻:當古希臘神話中的泰坦Atlas 把整個世界(地球)抗在他的肩膀上的時候,世界公民們(數據庫用戶)并不需要去感知這個世界有多沉重(數據庫有多復雜)。
任務5:復雜的圖算法。
相比于其他數據庫而言,圖數據庫的一個明顯優勢是集成化的算法功能支持。圖上有很多種算法,例如出入度、中心度、排序、傳播、連接度、社區識別、圖嵌入、圖神經元網絡等等。隨著商用場景的增多,相信會有更多的算法被移植到圖上或者被發明創造出來。
以魯汶社區識別算法為例,這個算法出現的時間僅僅十幾年,它得名于它的誕生地——比利時法語區的魯汶大學( )。
它最初被發明的目的是用來通過復雜的多次遞歸遍歷一張由社交關系屬性構成的大圖中的點、邊來找到所有的頂點(例如人、事、物)所構成的關聯關系社區,緊密關聯的頂點會處于同一社區,不同的頂點可能會處于不同的社區。在互聯網、金融科技領域,魯汶算法受到了相當的重視。下面這行 GQL 語句完成了魯汶算法的調用執行:
algo().louvain({phase1_loop:5, min_modularity_increase:0.01})
在圖數據庫中,調用一個算法與執行一個API調用類似,都需要提供一些必須的參數。上例中,用戶僅需提供最少兩個參數就可以執行魯汶。當然,用戶也可以設定更為復雜的參數集來優化魯汶算法,因篇幅所限,本文在此并不做過多展開描述。
魯汶算法因其天然的邏輯復雜性,計算結果與效果如果能通過可視化的方式來呈現,會起到事半功倍的效果。在下圖中,我們展示了一種在魯汶算法執行過程中自動化生成的數據集,可以支持基于全量數據或抽樣數據的實時算法結果3D可視化。
圖:實時的魯汶社區識別算法及Web可視化( )
注:原生的魯汶社區識別算法的實現是串行的,也就是說它需要從全圖中的所有頂點出發,逐個頂點、逐條邊的逐條邊進行反復運算。例如在的庫中,對一個普通的(幾十萬至幾百萬頂點)圖數據集進行魯汶運算要耗時數個、數十個小時sql 數據庫關系圖,但是在 Graph上,這個計算的耗時通過高度并發被劇烈的縮短到了毫秒到秒級。在這里,我們探討的不是10倍至100倍的性能超越,而是成千上萬倍的性能提升。果讀者覺得筆者給出的案例是天方夜譚或為癡人說夢,或許你應當重新審視一下你對于系統架構、數據結構、算法以及它們的最優工程實現的理解了。
圖查詢語言還可以支持很多功能強大且智能化很高深的操作,上面的5個例子只起到了一個拋磚引玉的作用,筆者希望它們能揭示GQL的簡潔性,并喚起讀者去思考一個問題:你到底是愿意去絞盡腦汁地書寫成百上千行的SQL代碼,并借此殺死大量腦細胞來讀懂代碼,還是考慮用更簡潔、方便卻更加強大的圖查詢語言呢?
關于數據庫查詢語言,筆者認為:
數據庫查詢語言不應該只是數據科學家、分析員的專有工具,任何業務人員都可以(并應該)掌握一門查詢語言。
查詢語言應當便于使用,所有數據庫底層的架構、工程實現的復雜性應當對于上層的用戶而言是透明的。
圖數據庫有巨大潛力,在未來的一段時間內會大幅替代SQL的負載,有一些業界頂級的公司,例如微軟和亞馬遜已經預估未來8-10年間,會有40%-50%的SQL負載會遷移到圖數據庫上完成。讓我們拭目以待。
有些人包括一些知名的投資機構和行業“專家”認為,關系型數據庫和SQL永遠也不會被取代。筆者認為這種看法禁不起推敲,如果我們稍微回顧一下不是很久遠的歷史就會發現,關系型數據庫從20世紀80年代開始取代了之前的導航型數據庫,已經稱霸了行業近半個世紀了,但是它們越來越難以滿足不斷迭代與前進的業務需求。
如果歷史真正教會我們一些常識,那就是對于任何事情的執著和癡迷都不會長久,特別是在這個科技蓬勃發展且不斷推陳出新的時代。·END·