“即使我感覺自己已經(jīng)掌握了很多圖查詢命令,但當我開始進行圖建模時,我還是感覺自己像個矮子。”
這是我們的一個客戶在著手從之前習慣使用的關系型數(shù)據(jù)庫切換到圖數(shù)據(jù)庫時所發(fā)表的感概。這位客戶所述并非孤例,因為在圖數(shù)據(jù)庫的廣袤世界中,很多初學者都曾歷經(jīng)此種滑稽境地。
當我們給圖數(shù)據(jù)庫新手做學習建議時,通常會強調查詢語言的重要性。但在實際應用中,大多數(shù)用戶的第一個難關并不在于如何編寫查詢語句,而是如何將數(shù)據(jù)順利遷移到圖數(shù)據(jù)庫——特別是從傳統(tǒng)的關系型數(shù)據(jù)庫切換到圖數(shù)據(jù)庫。
當用戶手上握有上百個字段分布在數(shù)十張關系表中時,數(shù)據(jù)遷移的任務無疑是一項相當艱巨的工程:
- 節(jié)點和邊的不當區(qū)分
數(shù)據(jù)表的命名常常會傾向于將所有表都呈現(xiàn)為真實世界中的不同實體類型,這將會導致用戶在試圖區(qū)分哪些表代表真實實體,哪些表代表這些實體之間的關系時感到困惑。
- 模式[1]中存在過多的冗余屬性
“冗余字段”的概念旨在解決SQL表JOIN查詢的低效率問題。由于這一概念在很多用戶的頭腦中根深蒂固,使得刪除或替換這些字段變得極具挑戰(zhàn)性,尤其是在進行圖數(shù)據(jù)的設計時。
- 重構表結構時缺乏大膽性
表結構的修改迭代往往需要歷經(jīng)多個輪次,最終獲得的圖模型有時會與原始表格結構大相徑庭。而很少有初學者敢于對表結構做出大刀闊斧的改變。
黃金定律
無論用戶手中的數(shù)據(jù)是否以表的形式存在,它們在很大程度上都無法直接轉化為圖數(shù)據(jù)。圖數(shù)據(jù)有著基本的要求:如果是點,那么它必須擁有一個ID,這個ID就像是它的身份證,唯一標識著它的存在;如果是邊,那么它必須有一個FROM和一個TO,這兩者的取值都必須是圖中某個點的ID。只有這樣,我們才能夠說:“啊,邊真是個奇妙的橋梁,能把點和點連接起來!”。

關于點ID和邊FROM、TO的示例
如果真有所謂的黃金定律能幫助我們順利地把表變成圖,那就是:“充分關注原始表中的主鍵和外鍵”,因為它們將成為節(jié)點的ID以及邊的FROM和TO,是成功得到圖數(shù)據(jù)的關鍵。
讓我們以一個實際案例為例,介紹如何從一個使用SQL Server搭建的醫(yī)院信息管理系統(tǒng)出發(fā),逐步將其表結構改造為圖模型,并將獲得的圖數(shù)據(jù)導入到嬴圖系統(tǒng)中。
SQL Server建表
這個簡單的醫(yī)院信息管理系統(tǒng)可以記錄醫(yī)生、患者、科室、病床的基本信息,追蹤醫(yī)生的接診情況以及患者的住院記錄。所有數(shù)據(jù)通過以下六張表來存儲:

醫(yī)院管理系統(tǒng)的表結構(SQL Server)
這些表的字段設計如下:

表之間的引用關系(從外鍵到主鍵)
如上圖所示,由外鍵指向主鍵的箭頭線既表達了表之間如何相連,也體現(xiàn)了SQL是如何進行表間關聯(lián)查詢的。比如,當我們想要查詢某醫(yī)生在特定時段內接診的病人的姓名、性別以及診斷結果時,就可以依靠表DIAGNOSIS和表PATIENT之間的字段PNO進行關聯(lián),通過這個關聯(lián),我們能夠找到我們需要的信息。對于具備SQL經(jīng)驗的讀者來說,這種表連接查詢應該是家常便飯了。
不過,上述表結構設計也存在一些不完善之處。比如,在表DIAGNOSIS中記錄的是醫(yī)生姓名DNAME,而非醫(yī)生工號DNO。這個設計瑕疵導致了一個外鍵的遺漏,使得表DIAGNOSIS無法與表DOCTOR建立關聯(lián),無法獲取關于醫(yī)生的詳細信息。當然,我們可以在表DIAGNOSIS中添加一些常用的醫(yī)生信息,比如科室名DEPART,以此來規(guī)避關聯(lián)查詢帶來的麻煩。然而,這種冗余存儲所帶來的磁盤占用、數(shù)據(jù)一致性校驗等問題同樣不容忽視。
在設計圖數(shù)據(jù)庫模型時,我們將不再受制于這種存儲糾結。
我們將一些測試數(shù)據(jù)插入到前面所介紹的表中,為了節(jié)省篇幅,把所有表的內容整合到一起就得到如下結果:

醫(yī)院管理系統(tǒng)所有表的全部數(shù)據(jù)
不可否認,表數(shù)據(jù)的呈現(xiàn)方式雖然機械,但其詳盡的列表形式可以讓每個字段都一目了然。然而對于數(shù)據(jù)之間究竟發(fā)生了什么,比如“哪個科室的醫(yī)生診治了哪個病人”、“哪個病人入住了哪個科室的哪個病房”,這種直觀性展示在表數(shù)據(jù)中就難以體現(xiàn)了。與即將構建的圖數(shù)據(jù)相比,這種差異將格外明顯。
在嬴圖中構圖
圖模型的設計絕不是一蹴而就,讓我們先來“抄作業(yè)”:把每一張表都定義為schema,每個字段都定義為schema的屬性。接下來的挑戰(zhàn)是如何區(qū)分點、邊。
從某種意義上講,所有的表都可以被視為真實世界中的某種實體,但只有最地道的實體才適合作為節(jié)點,因此我們只選取了DEPARTMENT、DOCTOR、BED、PATIENT作為節(jié)點,并將他們的主鍵作為節(jié)點的ID(用 _id 表示):

點、邊劃分
對于表INPATIENT,請不要將其誤認為PATIENT的一個子集,因為它本質上記錄的是病人和病床之間的關系,這從它具有兩個外鍵、分別指向了表PATIENT和表BED就能看出。所以我們將INPATIENT定義為邊,并將這兩個外鍵定義為邊的FROM和TO(用 _from、_to 表示),代表邊的起點和終點。
(邊的FROM和TO可以酌情對調,只要邊的含義說得通即可。如,當FROM代表PATIENT的ID、TO代表BED的ID時,邊表示“病人入住病床”,反之則邊表示“病床接收病人”。)
按照同樣思路,我們將DIAGNOSIS也定義為邊,因為它記錄了醫(yī)生和病人之間的關系。在定義FROM和TO時,由于原始表只有一個指向PATIENT的外鍵,缺少指向DOCTOR的外鍵,所以需要手將這個遺失的外鍵補齊。這一外鍵缺失的問題在之前介紹表結構時已經(jīng)分析過了,現(xiàn)在在邊的設計中得到了糾正。
不難看出,點、邊劃分的關鍵在于確定點的ID以及邊的FROM和TO,點的ID選取原表的主鍵,邊的FROM和TO則選取原表的外鍵(如缺少則先補充外鍵)。這便是我們在一開強調“充分關注原始表中的主鍵和外鍵”的原因。
有些好奇寶寶會問:“邊有沒有ID呢?”答案是:當然可以有了。事實上,我們通常也是把劃分為邊的表的主鍵作為邊的ID,這一邏輯和點ID的選取是相同的。我們不強調邊的ID,是因為對于邊來說FROM和TO是更為重要的。
當前所得的4個點模式和2個邊模式充分利用各自表中的主鍵和外鍵定義了點的ID和邊的FROM、TO。然而,在原始表中還有兩個外鍵未使用,分別是表DOCTOR和表BED的外鍵DEPART,它們都指向了DEPARTMENT的主鍵。那么,我們應該如何處理這些被劃分為點的表的外鍵呢?
一個簡單的方法是將這些外鍵定義為新的邊。如下圖所示,我們引入了兩種新的邊模式:WORKAT和BELONGTO。這種設計看起來就像是在原來的表結構中新增了兩張表,它們分別有兩個外鍵,這些外鍵最終指向DOCTOR、BED和DEPARTMENT:

引入新的邊模式WORKAT和BELONGTO
在引入WORKAT和BELONGTO之后,我們就可以將表DOCTOR和表BED的外鍵DEPART移除了。這兩種新的邊模式中除了FROM和TO之外就沒有其他屬性了,當然如果需要的話,也是可以添加更多屬性的,比如醫(yī)生在某科室工作的起止日期、某床位隸屬于某科室的起止日期1等,這樣就可以滿足實際情況中的醫(yī)生調換科室、病床轉移科室等需求。當然與這些實際情況相對應的原始表的結構也會和本文使用的例子有所不同。
至此我們已經(jīng)處理了表中所有的主鍵和外鍵,似乎已經(jīng)完成了從表結構到圖結構的轉化。然而,仔細回顧病床的表字段會發(fā)現(xiàn),其中還有個記錄病房號的字段RNUM。雖然它只是一個房間號,但它代表的是病房這種實體。在醫(yī)院管理中,有時需要統(tǒng)計病房的占用情況,或者管理病房的設施分配等,這些都要求病房被視為一種獨立的實體。為了滿足這個需求,我們將病房獨立拆分為一個新的點模式,稱之為ROOM:

提取點模式ROOM、共享邊模式BELONGTO
拆分之后,表BED和表INPATIENT均不再需要字段RNUM了,我們使用邊模式BELONGTO來表達的DEPARTMENT、ROOM和BED之間的隸屬關系。不光如此,DOCTOR和DEPARTMENT之間的邊WORKAS也可以用BELONGTO替代,反正這些邊都只有FROM和TO兩個屬性。
對于新添加的點模式ROOM,除了ID之外還可以有更多屬性,比如最多可容納的床位數(shù)量等。這些屬性可以結合實際需求來擴充,這樣,我們就不僅僅是簡單地將表結構轉換為圖結構,而是通過更精細地實體劃分來更加靈活地管理數(shù)據(jù)。
在確定了圖模型之后,下一步任務就是根據(jù)該模型對表數(shù)據(jù)進行清洗和改造,使其成為圖數(shù)據(jù)。假設每一張表的數(shù)據(jù)都獨立保存為一個CSV文件(如表DOCTOR的數(shù)據(jù)保存為DOCTOR.csv),文件表頭直接使用表字段的名稱,那么這個改造過程將涉及到表頭的修改、字段內容的替換以及新CSV文件的建立。具體步驟如下:
- 將DEPARTMENT.csv、PATIENT.csv、DOCTOR.csv、BED.csv各自主鍵所在列的表頭改名為_id
- 將IMPATIENT.csv的表頭PNO改名為_from,將表頭RBNUM改名為_to,將RNUM整列刪除
- 將DIAGNOSIS.csv的表頭PNO改名為_to,將表頭DNAME改名為_from并將其下的數(shù)據(jù)替換為DOCTOR.csv中_id下的數(shù)據(jù)
- 創(chuàng)建BELONGTO.csv,創(chuàng)建表頭_from和_to
- 將DOCTOR.csv中的_id、DEPART兩個表頭下的數(shù)據(jù)分別復制到BELONGTO.csv的_from和_to下,注意保持行內數(shù)據(jù)的對應關系,之后將DOCTOR.csv中的DEPART整列刪除
- 創(chuàng)建ROOM.csv,創(chuàng)建表頭_id,將BED.csv中RNUM下的數(shù)據(jù)去重后復制到ROOM.csv的_id之下
- 將BED.csv中的_id、RNUM下的數(shù)據(jù)分別復制到BELONGTO.csv的_from和_to下,注意保持行內數(shù)據(jù)的對應關系
- 將BED.csv中的RNUM、DEPART下的數(shù)據(jù)聯(lián)合去重后分別復制到BELONGTO.csv的_from和_to下,注意保持行內數(shù)據(jù)的對應關系,之后將BED.csv中的RNUM、DEPART整列刪除
請注意,表的主鍵值需要處理為全庫唯一[2]之后才能被用作圖數(shù)據(jù)的ID,與它們相關聯(lián)的外鍵也要同步修改。這也是我們很多用戶遇到的實際情況,盡管本文所使用的表數(shù)據(jù)并不存在這一問題。
得到圖數(shù)據(jù)之后,我們便可以將數(shù)據(jù)導入到嬴圖系統(tǒng)的某個圖集中。導入的方法有很多,可以通過嬴圖系統(tǒng)的可視化管理工具嬴圖Manager將文件逐個導入,也可以通過命令行工具嬴圖Transporter(Importer)將所有文件同時導入。數(shù)據(jù)導入之后,就可以在嬴圖Manager中查看圖模型與圖數(shù)據(jù)了:

嬴圖Manager將圖模型展示為schema和屬性列表

嬴圖Manager對圖數(shù)據(jù)的2D展示
嬴圖Manager的2D渲染功能使用鮮明的顏色和圖標令各種實體之間的關系清晰可見,我們只需一瞥就能輕松辨別圖中哪些節(jié)點之間存在密切聯(lián)系,比如可以從某個病床輕松追溯到其患者的主治醫(yī)生。這種直觀性和便捷性使圖數(shù)據(jù)庫在可視化方面的表現(xiàn)遠遠超過了關系型數(shù)據(jù)庫,讓數(shù)據(jù)分析和探索變得更加方便有趣。
結語
希望本文能夠為圖數(shù)據(jù)庫的初學者提供一些有關圖建模的啟示。在實際應用場景中,圖數(shù)據(jù)庫的圖模型構建還會涉及更加復雜的情況,圖模型本身也并非一成不變,需要在業(yè)務需求發(fā)生變化時進行靈活調整,以確保高效的查詢。最后,圖數(shù)據(jù)庫是一個充滿創(chuàng)造性和變化的領域,希望隨著經(jīng)驗的積累和學習的深入,我們終將更好地駕馭這個強大工具,為項目帶來更多可能性。
注釋:
[1] 模式:在某些關系型數(shù)據(jù)庫(如MySQL或PostgreSQL)中,一個服務器連接內數(shù)據(jù)管理的層級關系為“數(shù)據(jù)庫-模式-表-字段”,而在嬴圖系統(tǒng)中則為“圖-模式-屬性”。嬴圖系統(tǒng)中的模式相當于關系型數(shù)據(jù)庫中的表,模式的屬性相當于表的字段。當然這只是通過打比方來解釋嬴圖系統(tǒng)中的模式和屬性為何物。
[2] 嬴圖系統(tǒng)中的點ID和關系型數(shù)據(jù)庫中的表主鍵有區(qū)別。在嬴圖系統(tǒng)中,點的ID具有全圖唯一性,而不是在某一種模式中唯一。打個比方,如果abc是某個醫(yī)生的ID,那它就不能再作為某個患者或圖中任意一個節(jié)點的ID了。在關系型數(shù)據(jù)庫中,表的主鍵僅需在表內唯一,不同表的數(shù)據(jù)的主鍵是可以重復的。這使得在具體應用中,某些表的主鍵需要經(jīng)過處理之后才能被用作圖數(shù)據(jù)的ID,與它們相關聯(lián)的外鍵也要同步修改。