Categories
程式開發

一文了解主流圖數據庫查詢語言| 操作入門篇


圖數據庫已經越來越被人們熟知,同時也在許多企業中得到了應用,但是由於市面上沒有統一的圖查詢語言標準,所以有部分開發者對於不同圖數據庫的用法存在著疑問。因此本文作者對市面上主流的幾款圖數據庫進行了一番分析,並以查詢操作為例進行深入介紹。

文章的開頭我們先來看下什麼是圖數據庫,根據維基百科的定義:圖數據庫是使用圖結構進行語義查詢的數據庫,它使用節點、邊和屬性來表示和存儲數據

雖然和關係型數據庫存儲的結構不同(關係型數據庫為表結構,圖數據庫為圖結構),但不計各自的性能問題,關係型數據庫可以通過遞歸查詢或者組合其他SQL 語句(Join)完成圖查詢語言查詢節點關係操作。得益於 1987 年 SQL 成為國際標準化組織(ISO)標準,關係型數據庫行業得到了很好的發展。同60、70 年代的關係型數據庫類似,圖數據庫這個領域的查詢語言目前也沒有統一標準,雖然19 年9 月經過國際SQL 標準委員會投票表決,決定將圖查詢語言(Graph Query Language)納為一種新的數據庫查詢語言,但GQL 的製定仍需要一段時間。

一文了解主流圖數據庫查詢語言| 操作入門篇 1

鑑於市面上沒有統一的圖查詢語言標準,在本文中我們選取市面上主流的幾款圖查詢語言來分析一波用法,由於篇幅原因本文旨在簡單介紹圖查詢語言和常規用法,更詳細的內容將在進階篇中講述。

圖查詢語言·介紹

一文了解主流圖數據庫查詢語言| 操作入門篇 2

圖查詢語言 Gremlin

Gremlin 是 Apache ThinkerPop 框架下的圖遍歷語言。 Gremlin 可以是聲明性的也可以是命令性的。雖然 Gremlin 是基於 Groovy 的,但具有許多語言變體,允許開發人員以 Java、JavaScript、Python、Scala、Clojure 和 Groovy 等許多現代編程語言原生編寫 Gremlin 查詢

支持圖數據庫:Janus Graph、InfiniteGraph、Cosmos DB、DataStax Enterprise(5.0+)、Amazon Neptune

圖查詢語言 Cypher

Cypher 是一個描述性的圖形查詢語言,允許不必編寫圖形結構的遍歷代碼對圖形存儲有表現力和效率的查詢,和SQL 很相似,Cypher 語言的關鍵字不區分大小寫,但是屬性值,標籤,關係類型和變量是區分大小寫的。

支持圖數據庫: Neo4j、RedisGraph、AgensGraph

圖查詢語言 nGQL

nGQL 是一種類 SQL 的聲明型的文本查詢語言,nGQL 同樣是關鍵詞大小寫不敏感的查詢語言,目前支持模式匹配、聚合運算、圖計算,可無嵌入組合語句。

支持圖數據庫:Nebula Graph

圖查詢語言·術語篇

在比較這3 個圖查詢語言之前,我們先來看看他們各自的術語,如果你翻閱他們的文檔會經常見到下面這些“關鍵字”,在這裡我們不講用法,只看這些圖數據庫常用概念在這3 個圖數據庫文檔中的叫法。

術語 Gremlin Cypher nGQL
Vertex Node Vertex
Edge Relationship Edge
點類型 Label Label Tag
邊類型 label RelationshipType edge type
點 ID vid id(n) vid
邊 ID eid id®
插入 add create insert
刪除 drop delete delete / drop
更新屬性 setProperty set update

我們可以看到大體上對點和邊的叫法類似,只不過 Cypher 中直接使用了 Relationship 關係一詞代表邊。其他的術語基本都非常直觀。

圖查詢語言·實操篇

上面說了一通術語之類的“乾貨”之後,是時候展示真正的技術了——來個具體一點的例子,在具體的例子中我們將會分析 Gremlin、Cypher、nGQL 的用法不同。

示例圖:The Graphs of Gods

實操示例使用了 Janus Graph 的示例圖 The Graphs of Gods。該圖結構如下圖所示,描述了羅馬萬神話中諸神關係。

一文了解主流圖數據庫查詢語言| 操作入門篇 3

插入數據

# 插入点
## nGQL
nebula> INSERT VERTEX character(name, age, type) VALUES hash("saturn"):("saturn", 10000, "titan"), hash("jupiter"):("jupiter", 5000, "god");
## Gremlin
gremlin> saturn = g.addV("character").property(T.id, 1).property('name', 'saturn').property('age', 10000).property('type', 'titan').next();
==>v[1]
gremlin> jupiter = g.addV("character").property(T.id, 2).property('name', 'jupiter').property('age', 5000).property('type', 'god').next();
==>v[2]
gremlin> prometheus = g.addV("character").property(T.id, 31).property('name',  'prometheus').property('age', 1000).property('type', 'god').next();
==>v[31]
gremlin> jesus = g.addV("character").property(T.id, 32).property('name',  'jesus').property('age', 5000).property('type', 'god').next();
==>v[32]
## Cypher
cypher> CREATE (src:character {name:"saturn", age: 10000, type:"titan"})
cypher> CREATE (dst:character {name:"jupiter", age: 5000, type:"god"})
# 插入边
## nGQL
nebula> INSERT EDGE father() VALUES hash("jupiter")->hash("saturn"):();
## Gremlin
gremlin> g.addE("father").from(jupiter).to(saturn).property(T.id, 13);
==>e[13][2-father->1]
## Cypher
cypher> CREATE (src)-[rel:father]->(dst)

在數據插入這塊,我們可以看到 nGQL 使用 INSERT VERTEX 插入點,而 Gremlin 直接使用類函數的 g.addV() 來插入點,Cypher 使用 CREATE 這個 SQL 常見關鍵詞來創建插入的點。
在點對應的屬性值方面,nGQL 通過 VALUES 關鍵詞來賦值,Gremlin 則通過操作 .property() 進行對應屬性的賦值,Cypher 更直觀直接在對應的屬性值後面跟上想對應的值。

在邊插入方面,可以看到和點的使用語法類似,只不過在 Cypher 和 nGQL 中分別使用 -[]-> 和 **-> 來表示關係,而 Gremlin 則用 to() **關鍵詞來標識指向關係,在使用這 3 種圖查詢語言的圖數據庫中的邊均為有向邊,下圖左邊為有向邊,右邊為無向邊。

一文了解主流圖數據庫查詢語言| 操作入門篇 4

刪除數據

# nGQL
nebula> DELETE VERTEX hash("prometheus");
# Gremlin
gremlin> g.V(prometheus).drop();
# Cypher
cypher> MATCH (n:character {name:"prometheus"}) DETACH DELETE n 

這裡,我們可以看到大家的刪除關鍵詞都是類似的:Delete Drop,不過這裡需要注意的是上面術語篇中提過nGQL 中刪除操作對應單詞有Delete 和Drop ,在nGQL 中Delete 一般用於點邊,Drop 用於Schema 刪除,這點和SQL 的設計思路是一樣的。

更新數據

# nGQL
nebula> UPDATE VERTEX hash("jesus") SET character.type = 'titan';
# Gremlin
gremlin> g.V(jesus).property('age', 6000);
==>v[32]
# Cypher
cypher> MATCH (n:character {name:"jesus"}) SET n.type = 'titan';

可以看到Cypher 和nGQL 都使用SET 關鍵詞來設置點對應的類型值,只不過nGQL 中多了UPDATE 關鍵詞來標識操作,Gremlin 的操作和查看點操作類似,只不過增加了變更property 值操作,這裡我們注意到的是,Cypher 中常見的一個關鍵詞便是 MATCH,顧名思義,它是一個查詢關鍵詞,它會去選擇匹配對應條件下的點邊,再進行下一步操作。

查看數據

# nGQL
nebula> FETCH PROP ON character hash("saturn");
===================================================
| character.name | character.age | character.type |
===================================================
| saturn         | 10000         | titan          |
---------------------------------------------------
# Gremlin
gremlin> g.V(saturn).valueMap();
==>[name:[saturn],type:[titan],age:[10000]]
# Cypher
cypher> MATCH (n:character {name:"saturn"}) RETURN properties(n)
  ╒════════════════════════════════════════════╕
  │"properties(n)"                             │
  ╞════════════════════════════════════════════╡
  │{"name":"saturn","type":"titan","age":10000}│
  └────────────────────────────────────────────┘

在查看數據這塊,Gremlin 通過調取valueMap() 獲得對應的屬性值,而Cypher 正如上面更新數據所說,依舊是MATCH 關鍵詞來進行對應的匹配查詢再通過RETURN 返回對應的數值,而nGQL 則對saturn 進行hash 運算得到對應VID之後去獲取對應VID 的屬性值。

查詢 hercules 的父親

# nGQL
nebula>  LOOKUP ON character WHERE character.name == 'hercules' | 
      -> GO FROM $-.VertexID OVER father YIELD $$.character.name;
=====================
| $$.character.name |
=====================
| jupiter           |
---------------------
# Gremlin
gremlin> g.V().hasLabel('character').has('name','hercules').out('father').values('name');
==>jupiter
# Cypher
cypher> MATCH (src:character{name:"hercules"})-[:father]->(dst:character) RETURN dst.name
      ╒══════════╕
      │"dst.name"│
      ╞══════════╡
      │"jupiter" │
      └──────────┘

查詢父親,其實是一個查詢關係/邊的操作,這裡不做贅述,上面插入邊的時候簡單介紹了Gremlin、Cypher、nGQL 這三種圖數據庫是各自用來標識邊的關鍵詞和操作符是什麼。

查詢 hercules 的祖父

# nGQL
nebula> LOOKUP ON character WHERE character.name == 'hercules' | 
     -> GO 2 STEPS FROM $-.VertexID OVER father YIELD $$.character.name;
=====================
| $$.character.name |
=====================
| saturn            |
---------------------
# Gremlin
gremlin> g.V().hasLabel('character').has('name','hercules').out('father').out('father').values('name');
==>saturn
# Cypher
cypher> MATCH (src:character{name:"hercules"})-[:father*2]->(dst:character) RETURN dst.name
      ╒══════════╕
      │"dst.name"│
      ╞══════════╡
      │"saturn"  │
      └──────────┘

查詢祖父,其實是一個查詢對應點的兩跳關係,即:父親的父親,我們可以看到 Gremlin 使用了兩次 out() 來表示為祖父,而 nGQL 這裡使用了 (Pipe 管道)的概念,用於子查詢。在兩跳關係處理上,上面說到 Gremlin 是用了 2 次 out(),而 Cypher、nGQL 則引入了 step 數的概念,分別對應到查詢語句的 GO 2 STEP 和 [:father *2],相對來說 Cypher、nGQL 這樣書寫更優雅。

查詢年齡大於 100 的人物

# nGQL
nebula> LOOKUP ON character WHERE character.age > 100 YIELD character.name, character.age;
=========================================================
| VertexID             | character.name | character.age |
=========================================================
| 6761447489613431910  | pluto          | 4000          |
---------------------------------------------------------
| -5860788569139907963 | neptune        | 4500          |
---------------------------------------------------------
| 4863977009196259577  | jupiter        | 5000          |
---------------------------------------------------------
| -4316810810681305233 | saturn         | 10000         |
---------------------------------------------------------
# Gremlin
gremlin> g.V().hasLabel('character').has('age',gt(100)).values('name');
==>saturn
==>jupiter
==>neptune
==>pluto
# Cypher
cypher> MATCH (src:character) WHERE src.age > 100 RETURN src.name
      ╒═══════════╕
      │"src.name" │
      ╞═══════════╡
      │  "saturn" │
      ├───────────┤
      │ "jupiter" │
      ├───────────┤
      │ "neptune" │
      │───────────│
      │  "pluto"  │
      └───────────┘

這個是一個典型的查詢語句,找尋符合特定條件的點並返回結果,在Cypher 和nGQL 中用WHRER 進行條件判斷,而Gremlin 延續了它的“編程風”用gt(100) 表示年大於齡100 的這個篩選條件,延伸下Gremlin 中eq() 則表示等於這個查詢條件。

從一起居住的人物中排除 pluto 本人

# nGQL
nebula>  GO FROM hash("pluto") OVER lives YIELD lives._dst AS place | GO FROM $-.place OVER lives REVERSELY WHERE 
$$.character.name != "pluto" YIELD $$.character.name AS cohabitants;
===============
| cohabitants |
===============
| cerberus    |
---------------
# Gremlin
gremlin> g.V(pluto).out('lives').in('lives').where(is(neq(pluto))).values('name');
==>cerberus
# Cypher
cypher> MATCH (src:character{name:"pluto"})-[:lives]->()<-[:lives]-(dst:character) RETURN dst.name
      ╒══════════╕
      │"dst.name"│
      ╞══════════╡
      │"cerberus"│
      └──────────┘

這是一個沿指定點 Pluto 反向查詢指定邊(居住)的操作,在反向查詢中,Gremlin 使用了 in 來表示反向關係,而 Cypher 則更直觀的將指向箭頭反向變成 <- 來表示反向關係,nGQL 則用關鍵詞 REVERSELY 來標識反向關係。

Pluto 的兄弟們居住在哪

# which brother lives in which place?
## nGQL
nebula> GO FROM hash("pluto") OVER brother YIELD brother._dst AS god | 
GO FROM $-.god OVER lives YIELD $^.character.name AS Brother, $$.location.name AS Habitations;
=========================
| Brother | Habitations |
=========================
| jupiter | sky         |
-------------------------
| neptune | sea         |
-------------------------
## Gremlin
gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god','place').by('name');
==>[god:jupiter, place:sky]
==>[god:neptune, place:sea]
## Cypher
cypher> MATCH (src:Character{name:"pluto"})-[:brother]->(bro:Character)-[:lives]->(dst)
RETURN bro.name, dst.name
      ╒═════════════════════════╕
      │"bro.name"    │"dst.name"│
      ╞═════════════════════════╡
      │ "jupiter"    │  "sky"   │
      ├─────────────────────────┤
      │ "neptune"    │ "sea"    │
      └─────────────────────────┘

這是一個通過查詢指定點 Pluto 查詢指定邊 brother 後再查詢指定邊 live 的查詢,相對來說不是很複雜,這裡就不做解釋說明了。

最後,本文只是對 Gremlin、Cypher、nGQL 等 3 個圖查詢語言進行了簡單的介紹,更複雜的語法將在本系列的後續文章中繼續,歡迎在論壇留言交流。

附 錄