Nebula Graph 是一款开源的、分布式的、易扩展的原生图数据库,能够承载数千亿个点和数万亿条边的超大规模数据集,并且提供毫秒级查询。
1.1 Nebula 数据模型
- 图空间(Space):图空间是 Nebula Graph 中彼此隔离的图数据集合,与 MySQL 中的 database 概念类似。
- 点(Vertex):点用来保存实体对象,特点如下:
- 点是用点标识符(VID
或称为 Vertex ID
)标识的。VID
在同一图空间中唯一。VID 是一个 int64,或者 fixed_string(N)。 - 点必须有至少一个 Tag,也可以有多个 Tag,但不能没有 Tag。
- 点是用点标识符(VID
- 边(Edge):边是用来连接点的,表示两个点之间的关系或行为,特点如下:
- 两点之间可以有多条边。
- 边是有方向的,不存在无向边。
- 四元组 <起点VID、Edge type、边排序值(Rank)、终点VID>
用于唯一标识一条边。边没有 EID。 - 一条边有且仅有一个 Edge type。
- 一条边有且仅有一个 rank。其为 int64,默认为 0。
- 标签(Tag):点的类型,定义了一组描述点类型的属性。
- 边类型(Edge type):边的类型,定义了一组描述边的类型的属性。Tag 和 Edge type 的作用,类似于关系型数据库中“点表”和“边表”的表结构。
- 属性(Properties):属性是指以键值对(Key-value pair)形式存储的信息。
1.2 Nebula 架构总览
Nebula Graph 由三种服务构成:Graph 服务、Meta 服务和 Storage 服务,是一种存储与计算分离的架构。
- Graph 服务主要负责处理查询请求,包括解析查询语句、校验语句、生成执行计划以及按照执行计划执行四个大步骤。
- Meta 服务负责管理元数据信息,包括用户账号和权限信息、分片位置信息、图空间、Schema 信息、作业信息等等。
- Storage 服务负责数据的存储,通过 Raft 协议保证数据多副本之间的一致性。
1.3 Nebula 快速入门
本文将介绍在 Centos7 操作系统上通过 RPM 安装 Nebula。
1.3.1 安装 Nebula Graph
下载 RPM 安装包。
wget https://oss-cdn.nebula-graph.com.cn/package/2.6.1/nebula-graph-2.6.1.el7.x86_64.rpm
安装 RPM 包。
sudo rpm -ivh nebula-graph-2.6.1.el7.x86_64.rpm
1.3.2 启动 Nebula Graph 服务
Nebula Graph 使用脚本 nebula.service
管理服务,包括启动、停止、重启、中止和查看。
nebula.service
的默认路径是 /usr/local/nebula/scripts
,如果修改过安装路径,请使用实际路径。
nebula.service 脚本的语法如下。
sudo /usr/local/nebula/scripts/nebula.service
[-v] [-c <config_file_path>]
<start|stop|restart|kill|status>
<metad|graphd|storaged|all>
参数说明如下。
我们使用以下命令启用 Nebula Graph 的所有服务,包括 Meta 服务、Graph 服务和 Storage 服务。
sudo /usr/local/nebula/scripts/nebula.service start all
查看所有服务的状态,可以看到此时 Nebula Graph 的服务都已经正常启动。
sudo /usr/local/nebula/scripts/nebula.service status all
# 返回结果
[WARN] The maximum files allowed to open might be too few: 1024
[INFO] nebula-metad(de03025): Running as 62568, Listening on 9559
[INFO] nebula-graphd(de03025): Running as 62658, Listening on 9669
[INFO] nebula-storaged(de03025): Running as 62673, Listening on 9779
1.3.3 连接 Nebula Graph
Nebula Graph 支持多种类型客户端,包括 CLI 客户端、GUI 客户端和流行编程语言开发的客户端,详情可以查看 [Nebula Graph 生态工具概览] (https://docs.nebula-graph.com.cn/2.6.1/20.appendix/6.eco-tool-version/)。接下来将介绍如何使用原生 CLI 客户端 Nebula Console 来连接 Nebula Graph 数据库。
首先在 Github 的 [Nebula Console 下载页面] (https://github.com/vesoft-inc/nebula-console/releases) 根据机器的系统和 CPU 架构选择对应的二进制文件。我使用的机器的 CPU 架构是 x86_64 的,因此这里选择下载 amd64 的二进制文件。
为了方便使用,将文件重命名为 nebula-console。
wget https://github.com/vesoft-inc/nebula-console/releases/download/v2.6.0/nebula-console-linux-amd64-v2.6.0
mv nebula-console-linux-amd64-v2.6.0 nebula-console
为 nebula-console 二进制文件赋予可执行权限。
chmod +x nebula-console
nebula-console 的语法如下。
./nebula-console -addr <ip> -port <port> -u <username> -p <password> [-t 120] [-e "nGQL_statement" | -f filename.nGQL]
参数说明如下。
使用以下命令连接 Nebula Graph。
./nebula-console -addr 192.168.1.12 -port 9669 -u root -p nebula
看到以下输出说明连接成功。
1.3.4 使用常用命令
接下来将使用下图的数据集演示 Nebula Graph 基础的操作语法,包括用于 Schema 创建和常用增删改查操作的语句。nGQL(Nebula Graph Query Language)是 Nebula Graph 使用的的声明式图查询语言,支持灵活高效的图模式,而且 nGQL 是为开发和运维人员设计的类 SQL 查询语言,易于学习。
下表为 basketballplayer 数据集的结构示例,包括两种类型的点(player、team)和两种类型的边(serve、follow)。
本文将使用下图的数据集演示基础操作的语法。
1.3.4.1 创建和选择图空间
执行如下语句创建名为basketballplayer
的图空间。
(root@nebula) [(none)]> CREATE SPACE basketballplayer(partition_num=15, replica_factor=1, vid_type=fixed_string(30));
选择图空间basketballplayer
。
(root@nebula) [(none)]> USE basketballplayer;
查看创建的图空间。
(root@nebula) [basketballplayer]> SHOW SPACES;
+--------------------+
| Name |
+--------------------+
| "basketballplayer" |
+--------------------+
1.3.4.2 创建 Tag 和 Edge type
Tag 和 Edge type 的作用,类似于关系型数据库中“点表”和“边表”的表结构。创建 Tag: player
和 team
,以及 Edge type: follow
和 serve
。
CREATE TAG player(name string, age int);
CREATE TAG team(name string);
CREATE EDGE follow(degree int);
CREATE EDGE serve(start_year int, end_year int);
1.3.4.3 插入点和边
可以使用 INSERT
语句,基于现有的 Tag 插入点,或者基于现有的 Edge type 插入边。
插入代表球员和球队的点。
INSERT VERTEX player(name, age) VALUES "player100":("Tim Duncan", 42);
INSERT VERTEX player(name, age) VALUES "player101":("Tony Parker", 36);
INSERT VERTEX player(name, age) VALUES "player102":("LaMarcus Aldridge", 33);
INSERT VERTEX team(name) VALUES "team203":("Trail Blazers"), "team204":("Spurs");
插入代表球员和球队之间关系的边。
INSERT EDGE follow(degree) VALUES "player101" -> "player100":(95);
INSERT EDGE follow(degree) VALUES "player101" -> "player102":(90);
INSERT EDGE follow(degree) VALUES "player102" -> "player100":(75);
INSERT EDGE serve(start_year, end_year) VALUES "player101" -> "team204":(1999, 2018),"player102" -> "team203":(2006, 2015);
1.3.4.4 创建索引
MATCH
和 LOOKUP
语句的执行都依赖索引,但是索引会导致写性能大幅降低(降低 90% 甚至更多)。请不要随意在生产环境中使用索引,除非很清楚使用索引对业务的影响。
必须为“已写入但未构建索引”的数据重建索引,否则无法在 MATCH
和 LOOKUP
语句中返回这些数据,参见 [重建索引] (https://docs.nebula-graph.com.cn/2.6.1/3.ngql-guide/14.native-index-statements/4.rebuild-native-index/)。
原生索引可以基于指定的属性查询数据,创建原生索引分为以下 3 种情况:
- 创建 Tag/Edge type 索引。Tag 索引和 Edge type 索引应用于和Tag、Edge type 自身相关的查询,例如用 LOOKUP
查找有 Tag player
的所有点。 - 创建单属性索引。“属性索引”应用于基于属性的查询,例如基于属性 age
找到 age == 19
的所有的点。 - 创建复合属性索引(遵循”最左匹配原则”)。
关于创建索引的详细内容可以查看 [CREATE INDEX] (https://docs.nebula-graph.com.cn/2.6.1/3.ngql-guide/14.native-index-statements/1.create-native-index/#tagedge_type)
1.3.4.4.1 为 TAG 创建索引
为 TAG team 的创建索引,需要重建索引确保对已存在数据生效,注意在重建索引之前我们等待 20s,因为新创建的索引并不会立刻生效,因为创建索引是异步实现的,Nebula Graph 需要在下一个心跳周期才能完成索引的创建。
# 为 Tag team 创建索引 team_index_1。
CREATE TAG INDEX team_index_1 ON team();
# 重建索引确保能对已存在数据生效。
:sleep 20
REBUILD TAG INDEX team_index_1;
为 TAG player 的 name 属性创建单属性索引,为 name 和 age 属性创建复合属性索引。
# 为 Tag player 的 name 属性创建单属性索引 player_index_1。
# 索引长度为10。即只使用属性 name 的前 10 个字符来创建索引。
CREATE TAG INDEX player_index_1 ON player(name(20));
# 重建索引确保能对已存在数据生效。
REBUILD TAG INDEX player_index_1;
# 为 Tag player 的 name 和 age 属性创建复合属性索引 player_index_2。
CREATE TAG INDEX player_index_2 ON player(name,age);
# 重建索引确保能对已存在数据生效。
:sleep 20
REBUILD TAG INDEX player_index_2;
新创建的索引并不会立刻生效,创建新的索引并尝试立刻使用(例如 LOOKUP
或者 REBUILD INDEX
)通常会失败(报错 can’t find xxx in the space
)。因为创建步骤是异步实现的,Nebula Graph 要在下一个心跳周期才能完成索引的创建。可以使用如下方法之一:
- 1.在 SHOW TAG/EDGE INDEXES
语句的结果中查找到新的索引。 - 2.等待两个心跳周期,例如 20 秒。如果需要修改心跳间隔,请为所有配置文件修改参数 heartbeat_interval_secs
。
1.3.4.4.2 为 EDGE type 创建索引
为 EDGE type 创建索引的方式和点相同,只是把关键字改成 EDGE 即可。
# 为 EDGE follow 的 degree 属性创建索引,并重建索引。
CREATE EDGE INDEX follow_index_1 on follow(degree);
:sleep 20
REBUILD EDGE INDEX follow_index_1;
# 为 EDGE serve 创建索引,并重建索引。
CREATE EDGE INDEX serve_index_1 on serve();
:sleep 20
REBUILD EDGE INDEX serve_index_1;
# 为 EDGE serve 创建复合属性索引,并重建索引。
CREATE EDGE INDEX serve_index_2 on serve(start_year,end_year);
:sleep 20
REBUILD EDGE INDEX serve_index_2;
1.3.4.5 查看索引
查看为 TAG player 和 team 创建的索引。
(root@nebula) [basketballplayer]> SHOW TAG INDEXES;
+------------------+----------+-----------------+
| Index Name | By Tag | Columns |
+------------------+----------+-----------------+
| "player_index_1" | "player" | ["name"] | # 单属性索引
| "player_index_2" | "player" | ["name", "age"] | # 复合属性索引
| "team_index_1" | "team" | [] | # TAG 索引
+------------------+----------+-----------------+
查看为 EDGE follow 和 serve 创建的索引。
(root@nebula) [basketballplayer]> SHOW EDGE INDEXES;
+------------------+----------+----------------------------+
| Index Name | By Edge | Columns |
+------------------+----------+----------------------------+
| "follow_index_1" | "follow" | ["degree"] | # 单属性索引
| "serve_index_1" | "serve" | [] | # EDGE 索引
| "serve_index_2" | "serve" | ["start_year", "end_year"] | # 复合属性索引
+------------------+----------+----------------------------+
1.3.4.6 删除索引
删除 TAG player 的索引 player_index_2。
(root@nebula) [basketballplayer]> DROP TAG INDEX player_index_2;
删除 EDGE serve 的索引 serve_index_2。
(root@nebula) [basketballplayer]> DROP EDGE INDEX serve_index_2;
1.3.4.7 查询数据
查询数据主要有以下 4 种语句:
- GO 语句可以根据指定的条件遍历数据库。GO
语句从一个或多个点开始,沿着一条或多条边遍历,可以使用 YIELD
子句中指定的返回的信息。 - FETCH 语句可以获得点或边的属性。
- LOOKUP 语句是基于索引的,和 WHERE
子句一起使用,查找符合特定条件的数据。 - MATCH 语句是查询图数据最常用的,与 GO
或 LOOKUP
等其他查询语句相比,MATCH
的语法更灵活。MATCH 语句可以描述各种图模式,它依赖索引去匹配 Nebula Graph 中的数据模型。
1.3.4.7.1 GO 语句示例
从 TAG player 中 VID 为 player101
的球员开始,沿着边 follow
找到连接的球员。
(root@nebula) [basketballplayer]> GO FROM "player101" OVER follow;
+-------------+
| follow._dst |
+-------------+
| "player100" |
| "player102" |
+-------------+
1.3.4.7.2 FETCH 语句示例
查询 TAG player 中 VID 为 player100
的球员的属性值。
(root@nebula) [basketballplayer]> FETCH PROP ON player "player100";
+----------------------------------------------------+
| vertices_ |
+----------------------------------------------------+
| ("player100" :player{age: 42, name: "Tim Duncan"}) |
+----------------------------------------------------+
获取连接 player102 和 team203 的边 serve 的所有属性值。
(root@nebula) [basketballplayer]> FETCH PROP ON serve "player102" -> "team203";
+-----------------------------------------------------------------------+
| edges_ |
+-----------------------------------------------------------------------+
| [:serve "player102"->"team203" @0 {end_year: 2015, start_year: 2006}] |
+-----------------------------------------------------------------------+
1.3.4.7.3 LOOKUP 语句示例
列出 TAG player 的所有 VID。
(root@nebula) [basketballplayer]> LOOKUP ON player;
+-------------+
| VertexID |
+-------------+
| "player100" |
| "player102" |
| "player103" |
+-------------+
列出 EDGE serve 所有边的起始点、目的点和 rank。
(root@nebula) [basketballplayer]> LOOKUP ON serve;
+-------------+-----------+---------+
| SrcVID | DstVID | Ranking |
+-------------+-----------+---------+
| "player101" | "team204" | 0 |
| "player102" | "team203" | 0 |
+-------------+-----------+---------+
LOOKUP 也可以基于 where 条件进行过滤,例如在 EDGE serve 中查询 start_year == 2006 的属性值。
(root@nebula) [basketballplayer]> LOOKUP ON serve where serve.start_year == 2006;
+-------------+-----------+---------+
| SrcVID | DstVID | Ranking |
+-------------+-----------+---------+
| "player102" | "team203" | 0 |
+-------------+-----------+---------+
1.3.4.7.4 MATCH 语句示例
通过 MATCH 语句分别查询 TAG player 和 team 的属性值。
# 查询 Tag 为 player 的点的属性值
(root@nebula) [basketballplayer]> MATCH (x:player) return x;
+-----------------------------------------------------------+
| x |
+-----------------------------------------------------------+
| ("player100" :player{age: 42, name: "Tim Duncan"}) |
| ("player101" :player{age: 36, name: "Tony Parker"}) |
| ("player102" :player{age: 33, name: "LaMarcus Aldridge"}) |
+-----------------------------------------------------------+
# 查询 Tag 为 team 的点的属性值
(root@nebula) [basketballplayer]> MATCH (x:team) return x;
+------------------------------------------+
| x |
+------------------------------------------+
| ("team203" :team{name: "Trail Blazers"}) |
| ("team204" :team{name: "Spurs"}) |
+------------------------------------------+
也可以根据索引所在的属性进行查询,例如我们查询 TAG player 的 name 字段名为 Tony parker 的属性值。
(root@nebula) [basketballplayer]> MATCH (v:player{name:"Tony Parker"}) RETURN v;
+-----------------------------------------------------+
| v |
+-----------------------------------------------------+
| ("player101" :player{age: 36, name: "Tony Parker"}) |
+-----------------------------------------------------+
由于 TAG team 上的 name 属性并没有建立索引,因此无法根据 name 属性进行查询。
(root@nebula) [basketballplayer]> MATCH (v:team{name:'Spurs'}) RETURN v;
[ERROR (-1005)]: IndexNotFound: No valid index found
使用 MATCH 查询 EDGE serve 的属性值。
(root@nebula) [basketballplayer]> MATCH ()-[e:serve]-() RETURN e;
+-----------------------------------------------------------------------+
| e |
+-----------------------------------------------------------------------+
| [:serve "player101"->"team204" @0 {end_year: 2018, start_year: 1999}] |
| [:serve "player102"->"team203" @0 {end_year: 2015, start_year: 2006}] |
+-----------------------------------------------------------------------+
1.3.4.7.5 实际的查询例子
使用以下语句查询和 Tony Parker 有关的球员和球队。
(root@nebula) [basketballplayer]> MATCH p=(v:player{name:"Tony Parker"})-->(v2) RETURN p
+-------------------------------------------------------------------------------------------------------------------------------------------+
| p |
+-------------------------------------------------------------------------------------------------------------------------------------------+
| <("player101" :player{age: 36, name: "Tony Parker"})-[:serve@0 {end_year: 2018, start_year: 1999}]->("team204" :team{name: "Spurs"})> |
| <("player101" :player{age: 36, name: "Tony Parker"})-[:follow@0 {degree: 95}]->("player100" :player{age: 42, name: "Tim Duncan"})> |
| <("player101" :player{age: 36, name: "Tony Parker"})-[:follow@0 {degree: 90}]->("player102" :player{age: 33, name: "LaMarcus Aldridge"})> |
+-------------------------------------------------------------------------------------------------------------------------------------------+
和 Tony Parker 有关系的球员和球队在下图中用绿色方框标识。
1.3.4.8 修改点和边
用户可以使用 UPDATE
语句或 UPSERT
语句修改现有数据。UPSERT
是 UPDATE
和 INSERT
的结合体。当使用 UPSERT
更新一个点或边,如果它不存在,数据库会自动插入一个新的点或边。
首先查询 TAG player 现在的属性值。
(root@nebula) [basketballplayer]> match (n:player) return n;
+-----------------------------------------------------------+
| n |
+-----------------------------------------------------------+
| ("player100" :player{age: 42, name: "Tim"}) |
| ("player101" :player{age: 36, name: "Tony Parker"}) |
| ("player102" :player{age: 33, name: "LaMarcus Aldridge"}) |
+-----------------------------------------------------------+
用 UPDATE
修改 VID 为 player100
的球员的 name
属性,然后用 FETCH
语句检查结果。
(root@nebula) [basketballplayer]> UPDATE VERTEX "player100" SET player.name = "Tim";
(root@nebula) [basketballplayer]> FETCH PROP ON player "player100";
+---------------------------------------------+
| vertices_ |
+---------------------------------------------+
| ("player100" :player{age: 42, name: "Tim"}) |
+---------------------------------------------+
执行 UPSERT
语句,分别对已存在的 player101 和未存在的 player103 进行操作,通过 MATCH 查询可以看到在 UPSERT
修改了原本 player101 的值,新插入的 player103。
(root@nebula) [basketballplayer]> UPSERT VERTEX "player101" SET player.name = "CRIS", player.age = 18;
(root@nebula) [basketballplayer]> UPSERT VERTEX "player103" SET player.name = "THOMAS", player.age = 20;
(root@nebula) [basketballplayer]> match (n:player) return n;
+-----------------------------------------------------------+
| n |
+-----------------------------------------------------------+
| ("player101" :player{age: 18, name: "CRIS"}) |
| ("player100" :player{age: 42, name: "Tim"}) |
| ("player102" :player{age: 33, name: "LaMarcus Aldridge"}) |
| ("player103" :player{age: 20, name: "THOMAS"}) |
+-----------------------------------------------------------+
1.3.4.9 删除点和边
删除点。
nebula> DELETE VERTEX "player101";
删除边。
nebula> DELETE EDGE follow "player101" -> "team204";
1.3.4.10 删除 TAG 和 EDGE
删除 TAG/EDGE 前要确保 TAG/EDGE 不包含任何索引,否则 DROP TAG
时会报冲突错误 [ERROR (-8)]: Conflict!
删除 TAG。
# 删除 TAG 的索引
DROP TAG INDEX player_index_1;
DROP TAG INDEX team_index_1;
# 删除 TAG
DROP TAG player;
DROP TAG team;
删除 EDGE。
# 删除 EDGE 的索引
DROP EDGE INDEX follow_index_1
DROP EDGE INDEX serve_index_1
# 删除 EDGE
DROP EDGE follow;
DROP EDGE serve;