索引是数据库系统中不可或缺的一个功能,数据库索引比如是书的目录,能加快数据库的查询速度,其实质是数据库管理系统中一个排序的数据结构。不一样的数据库系统有不一样的排序结构,目前常见的索引实现类型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各类索引类型都有各自的排序算法。git
虽然索引能够带来更高的查询性能,可是也存在一些缺点,例如:github
Nebula Graph 做为一个高性能的分布式图数据库,对于属性值的高性能查询,一样也实现了索引功能。本文将对 Nebula Graph的索引功能作一个详细介绍。算法
开始以前,这里罗列一些可能会使用到的图数据库和 Nebula Graph 专有术语:数据库
Nebula Graph 是一个图数据库系统,查询场景通常是由一个点出发,找出指定边类型的相关点的集合,以此类推动行(广度优先遍历)N 度查询。另外一种查询场景是给定一个属性值,找出符合这个属性值的全部的点或边。在后面这种场景中,须要对属性值进行高性能的扫描,查出与此属性值对应的边或点,以及边或点上的其它属性。为了提升属性值的查询效率,在这里引入了索引的功能。对边或点的属性值进行排序,以便快速的定位到某个属性上。以此避免了全表扫描。bash
能够看到对图数据库 Nebula Graph 的索引要求:数据结构
从架构图能够看到,每一个Storage Server 中能够包含多个 Storage Engine, 每一个 Storage Engine中能够包含多个Partition, 不一样的Partition之间经过 Raft 协议进行一致性同步。每一个 Partition 中既包含了 data,也包含了 index,同一个点或边的 data 和 index 将被存储到同一个 Partition 中。架构
为了更好的描述索引的存储结构,这里将图数据库 Nebula Graph 原始数据的存储结构一块儿拿出来分析下。分布式
Vertex 的索引结构如上表所示,下面来详细地讲述下字段:性能
PartitionId:一个点的数据和索引在逻辑上是存放到同一个分区中的。之因此这么作的缘由主要有两点:优化
IndexId:index 的识别码,经过 indexId 可获取指定 index 的元数据信息,例如:index 所关联的 TagId,index 所在列的信息。
Index binary:index 的核心存储结构,是全部 index 相关列属性值的字节编码,详细结构将在本文的 #Index binary# 章节中讲解。
VertexId:点的识别码,在实际的 data 中,一个点可能会有不一样 version 的多行数据。可是在 index 中,index 没有 Version 的概念,index 始终与最新 Version 的 Tag 所对应。
上面讲完字段,咱们来简单地实践分析一波:
假设 PartitionId 为 _100,TagId 有 tag_1 和 tag_2,_其中 tag_1 包含三列 :col_t1_一、col_t1_二、col_t1_3,tag_2 包含两列:col_t2_一、col_t2_2。
如今咱们来建立索引:
能够看到虽然 tag_1 中有 col_t1_3 这列,可是创建索引的时候并无使用到 col_t1_3,由于在图数据库 Nebula Graph 中索引能够基于 Tag 的一列或多列进行建立。
// VertexId = hash("v_t1_1"),假如为 50 INSERT VERTEX tag_1(col_t1_1, col_t1_2, col_t1_3), tag_2(col_t2_1, col_t2_2) \ VALUES hash("v_t1_1"):("v_t1_1", "v_t1_2", "v_t1_3", "v_t2_1", "v_t2_2");
从上能够看到 VertexId 可由 ID 标识对应的数值通过 Hash 获得,若是标识对应的数值自己已经为 int64,则无需进行 Hash 或者其余转化数值为 int64 的运算。而此时数据存储以下:
此时点的 Data 结构
此时点的 Index 结构
说明:index 中 row 和 key 是一个概念,为索引的惟一标识;
边的索引结构和点索引结构原理相似,这里再也不赘述。但有一点须要说明,为了使索引 key 的惟一性成立,索引的 key 的生成借助了很多 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,这也是为何点索引中并无 TagId 字段(边索引中也没有 EdgeType 字段),这是由于** IndexId 自己带有 VertexId 等信息可直接区分具体的 tagId 或 EdgeType**。
Index binary 是 index 的核心字段,在 index binary 中区分定长字段和不定长字段,int、double、bool 为定长字段,string 则为不定长字段。因为** index binary 是将全部 index column 的属性值编码链接存储**,为了精确地定位不定长字段,Nebula Graph 在 index binary 末尾用 int32 记录了不定长字段的长度。
举个例子:
咱们如今有一个 index binary 为 index1,是由 int 类型的索引列1 c一、string 类型的索引列 c2,string 类型的索引列 c3 组成:
index1 (c1:int, c2:string, c3:string)
假如索引列 c一、c二、c3 某一行对应的 property 值分别为:2三、"abc"、"here",则在 index1 中这些索引列将被存储为以下(在示例中为了便于理解,咱们直接用原值,实际存储中是原值会通过编码再存储):
因此 index1 该 row 对应的 key 则为 23abchere34;
回到咱们 Index binary 章节开篇说的 index binary 格式中存在 Variable-length field lenght
字段,那么这个字段的的具体做用是什么呢?咱们来简单地举个例:
如今咱们又有了一个 index binary,咱们给它取名为 index2,它由 string 类型的索引列1 c一、string 类型的索引列 c2,string 类型的索引列 c3 组成:
index2 (c1:string, c2:string, c3:string)
假设咱们如今 c一、c二、c3 分别有两组以下的数值:
能够看到这两行的 prefix(上图红色部分)是相同,都是 "ababab",这时候怎么区分这两个 row 的 index binary 的 key 呢?别担忧,咱们有 Variable-length field lenght
。
若遇到 where c1 == "ab" 这样的条件查询语句,在 Variable-length field length 中可直接根据顺序读取出 c1 的长度,再根据这个长度取出 row1 和 row2 中 c1 的值,分别是 "ab" 和 "aba" ,这样咱们就精准地判断出只有 row1 中的 "ab" 是符合查询条件的。
当 Tag / Edge中的一列或多列建立了索引后,一旦涉及到 Tag / Edge 相关的写操做时,对应的索引必须连同数据一块儿被修改。下面将对索引的write操做在storage层的处理逻辑进行简单介绍:
当用户产生插入点/边操做时,insertProcessor 首先会判断所插入的数据是否有存在索引的 Tag 属性 / Edge 属性。若是没有关联的属性列索引,则按常规方式生成新 Version,并将数据 put 到 Storage Engine;若是有关联的属性列索引,则经过原子操做写入 Data 和 Index,并判断当前的 Vertex / Edge 是否有旧的属性值,若是有,则一并在原子操做中删除旧属性值。
当用户发生 Drop Vertex / Edge 操做时,deleteProcessor 会将 Data 和 Index(若是存在)一并删除,在删除的过程当中一样须要使用原子操做。
Vertex / Edge 的更新操做对于 Index 来讲,则是 drop 和 insert 的操做:删除旧的索引,插入新的索引,为了保证数据的一致性,一样须要在原子操做中进行。可是对应普通的 Data 来讲,仅仅是 insert 操做,使用最新 Version 的 Data 覆盖旧 Version 的 data 便可。
在图数据库 Nebula Graph 中是用 LOOKUP
语句来处理 index scan 操做的,LOOKUP
语句可经过属性值做为判断条件,查出全部符合条件的点/边,一样 LOOKUP
语句支持 WHERE
和 YIELD
子句。
正如根据本文#数据存储结构#章节所描述那样,index 中的索引列是按照建立 index 时的列顺序决定。
举个例子,咱们如今有 tag (col1, col2),根据这个 tag 咱们能够建立不一样的索引,例如:
咱们能够对 clo一、col2 创建多个索引,但在 scan index 时,上述四个 index 返回结果存在差别,甚至是彻底不一样,在实际业务中具体使用哪一个 index,及 index 的最优执行策略,则是经过索引优化器决定。
下面咱们再来根据刚才 4 个 index 的例子深刻分析一波:
lookup on tag where tag.col1 ==1 # 最优的 index 是 index1 lookup on tag where tag.col2 == 2 # 最优的 index 是index2 lookup on tag where tag.col1 > 1 and tag.col2 == 1 # index3 和 index4 都是有效的 index,而 index1 和 index2 则无效
在上述第三个例子中,index3 和 index4 都是有效 index,但最终必需要从二者中选出来一个做为 index,根据优化规则,由于 tag.col2 == 1 是一个等价查询,所以优先使用 tag.col2 会更高效,因此优化器应该选出 index4 为最优 index。
在这部分咱们就不具体讲解某个语句的用途是什么了,若是你对语句不清楚的话能够去图数据库 Nebula Graph 的官方论坛进行提问:https://discuss.nebula-graph.io/
(user@127.0.0.1:6999) [(none)]> CREATE SPACE my_space(partition_num=3, replica_factor=1); Execution succeeded (Time spent: 15.566/16.602 ms) Thu Feb 20 12:46:38 2020 (user@127.0.0.1:6999) [(none)]> USE my_space; Execution succeeded (Time spent: 7.681/8.303 ms) Thu Feb 20 12:46:51 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG lookup_tag_1(col1 string, col2 string, col3 string); Execution succeeded (Time spent: 12.228/12.931 ms) Thu Feb 20 12:47:05 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG INDEX t_index_1 ON lookup_tag_1(col1, col2, col3); Execution succeeded (Time spent: 1.639/2.271 ms) Thu Feb 20 12:47:22 2020
(user@127.0.0.1:6999) [my_space]> DROP TAG INDEX t_index_1; Execution succeeded (Time spent: 4.147/5.192 ms) Sat Feb 22 11:30:35 2020
若是你是从较老版本的 Nebula Graph 升级上来,或者用 Spark Writer 批量写入过程当中(为了性能)没有打开索引,那么这些数据尚未创建过索引,这时可使用 REBUILD INDEX 命令来从新全量创建一次索引。这个过程可能会耗时比较久,在 rebuild index 完成前,客户端的读写速度都会变慢。
REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE]
须要说明一下,使用 LOOKUP 语句前,请确保已经创建过索引(CREATE INDEX 或 REBUILD INDEX)。
(user@127.0.0.1:6999) [my_space]> INSERT VERTEX lookup_tag_1(col1, col2, col3) VALUES 200:("col1_200", "col2_200", "col3_200"), 201:("col1_201", "col2_201", "col3_201"), 202:("col1_202", "col2_202", "col3_202"); Execution succeeded (Time spent: 18.185/19.267 ms) Thu Feb 20 12:49:44 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200"; ============ | VertexID | ============ | 200 | ------------ Got 1 rows (Time spent: 12.001/12.64 ms) Thu Feb 20 12:49:54 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200" YIELD lookup_tag_1.col1, lookup_tag_1.col2, lookup_tag_1.col3; ======================================================================== | VertexID | lookup_tag_1.col1 | lookup_tag_1.col2 | lookup_tag_1.col3 | ======================================================================== | 200 | col1_200 | col2_200 | col3_200 | ------------------------------------------------------------------------ Got 1 rows (Time spent: 3.679/4.657 ms) Thu Feb 20 12:50:36 2020
索引的介绍就到此为止了,若是你对图数据库 Nebula Graph 的索引有更多的功能要求或者建议反馈,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向咱们提 issue 或者前往官方论坛:https://discuss.nebula-graph.io/ 的 Feedback
分类下提建议 👏
做者有话说:Hi,我是 bright-starry-sky,是图数据 Nebula Graph 研发工程师,对数据库存储有浓厚的兴趣,但愿本次的经验分享能给你们带来帮助,若有不当之处也但愿能帮忙纠正,谢谢~