免索引邻接vs混合索引的图数据库

本文的成因是缘于一篇图数据库厂商ArangoDB的英文原作,在直接翻译之前,会先解释下免索引邻接、图存储引擎、图处理引擎这几个概念,以便让大家有个基础认知。

因为这几个概念不管你怎么搜索,大部分内容千篇一律,基本是来自于Neo4j出品的两本书(《图数据库》、《Neo4j实战》)里宣讲的。虽然有些概念本身确实是有其技术先进性,但是它们固有的缺陷也是需要我们直面并认真对待的,不然我们会陷在图数据库某些表面光鲜的坑里而不自知。

另外,原文对于免索引邻接虽然有一定范围的展开,概念的理解和思路有很多借鉴意义,但是厂商思路大家懂得,可以有所取舍的阅读。我也会适当的添加一些注解,方便大家对原文的某些措辞或者描述进行进一步的解读。

 

- 概念解释

我们想象一下关系型数据库常见的例子,员工和部门,PDM如图:

 

employee 表会有一个 department 的外键关系, 不仅如此,外键就意味着很大可能性需要关联查询,那么employee.department 字段上我们通常会加上索引,用于快速检索。发现了么? 在这个模型里,没有一个实体来承载employeedepartment之间的关系,若两个实体进行关联查询时,我们依靠的两张表内索引的快速检索来找到关联记录的。

 

同样的,如果修改下PDM,在员工和部门之间增加一个映射关系表,感觉上relationship终于有一张表来承载了,但是你想象下三张表的关联查询,其实本质仍旧依赖的是每张表上的索引。


这也就是我们在一直宣讲的理念,为什么在图数据库中#Relationship是一等公民#,因为关联的节点在图数据库里是物理意义上的指向彼此,而免索引邻接就是基于这个指针可以直接找到关联数据,这就是不需要索引”“通过指针邻接彼此的概念了。

 

更多英文解释可以自行谷歌,下图中第二点把一个顶点视作一个迷你的索引也是种不错的思路。

图数据库的基本架构包括图的存储引擎和计算引擎:

1

图存储引擎 Storage

明确一点,不是所有的图数据库使用的都是为了性能优化过的原生图存储,你甚至可以用MySQL来存储图。图存储往往采用比如邻接列表或者邻接矩阵的结构。

邻接矩阵

https://en.wikipedia.org/wiki/Adjacency_matrix

邻接列表

https://en.wikipedia.org/wiki/Adjacency_list

使用RDBMS来实现图的存储演示

https://www.slideshare.net/quipo/rdbms-in-the-social-networks-age/64-Recursive_CTEsWITH_RECURSIVE_hierarchy_AS

2

图处理引擎 Processing

图处理引擎主要指的是通过各种图算法解决大规模分布式图计算问题。大部分的分布式图计算引擎都是基于Google发布的Pregel白皮书(讲述Google如何使用图计算引擎来计算网页排名)。

图本质上是一种递归的数据结构,其顶点的属性值依赖于其邻接顶点,而其邻接顶点属性又依赖于其邻接顶点,许多重要的图算法通过迭代计算每个顶点的属性直到到达定点条件,这些迭代的图算法被抽象成一系列图并行操作。

 

如图示,OrientDBNeo4j使用的是原生图存储和原生图处理引擎, 而 Titan 的后端存储可以使用HBase, Cassandra等列式存储。


 

 

- 译文开始

一些图数据库厂商宣传免索引邻接用于图模型的实现。维基百科上已经有了一些讨论:到底是什么让一个数据库称之为图数据库。这些厂商试图将无索引邻接这个概念推广为图数据库的基础,但已遭到社区的阻止。

因此,图形数据库在Wiki上仍然被定义为一种使用图结构进行数据展示和存储的数据库,这种结构结合节点、边和属性能够进行语义查询“- 不依赖于数据在内部的存储方式,而只是与模型和实现算法有关。

 

超级节点示例图

 

不过,问题依然存在:作为一个具体实现,免索引邻接对于图数据库是否还是一个合适的概念。这个概念表达的是,每个节点包括它所有边Edge的指针列表,因此避免了查找。在分布式世界里,很明显这种定义是毫无意义的,因为边Edge很可能分布在不同的服务器上, 也就理所当然不存在所谓这个边Edge指针(注:这里指的应该是不存在物理的跨服务器指针,不可以直接访问,仍旧需要网络IO)。在这种情况下,所有下文讨论的潜在优化相较于网络延迟都是微不足道的,所以分布场景需要更加聪明的算法。

因此,本文我们将重点关注单机场景,并深入挖掘它背后的计算机技术。

免索引邻接的重点是,整张图的遍历复杂度是 O(n), n 是节点数。相对的,使用索引的复杂度是 O(nlogn)。乍听起来似乎很合理,其实完全错误。使用Hash和链表来实现一个新型的索引(novel index),也可能获得相同的遍历复杂度 O(n)

(译者注:类似于上面介绍过的邻接链表,区别只是链表第一个节点是hash值。这样Hash检索的复杂度是O(1), 链表遍历的复杂度是O(n),所以总复杂度是O(n)

然而,免索引邻接有一些严重的缺陷。它的缺陷就在存在超级节点的时候。访问数据可以迅速的完成,但删除或更新节点上的边就变成了恶梦。比如一个典型的社交网络,一个明星或者名人(即超级节点)会拥有大量的关注(Followers)。如果不幸命中了这个超级节点,那么如果没有索引的话,更新操作意味着会变得异常复杂。

(译者注:这里夸大索引的作用有点突兀,其实对于超级节点的并发更新就算是使用链表也是避不开的)

如何能够做得更好呢?是的,通过使用新型的混合索引,我们就可以做到两全其美。通过组合哈希索引和链表,我们就能达到与免索引邻接实现相同的访问速度,同时可以更有效的执行删除或修改,尤其处理超级节点。让我们来探索下这种方法背后的复杂度分析。

 

对于每个独立节点,如果所有有关系的其他顶点视作一个一个的指针,并作为指针列表存储在独立节点上,假设该独立节点拥有 K 条边,则遍历所有邻节点的复杂度是O(k)。请注意这是最可能的复杂度因为 O(k) 就是基于遍历节点已然确定的大小。但考虑只是删除一条边的场景,仍旧是相同的 O(k)复杂度(假设是一个双向链表),这就成了我们不想看到的糟糕情况了。

(译者注:这里指的删除一条边的理想复杂度应该是O(1), 而不是遍历链表的 (<=K) 条边才找到要删除的那条)

此外,通常我们会希望能够做到双向遍历,这样有关系的两个顶点只有一条边,但两者不得不都存储这些指针。结果是,删除一个超级节点将出现相当糟糕的情况:为了删除所有有关的边,你不得不访问所有相邻的节点 - 然后执行一个可能异常昂贵的操作,一个一个的删除它们。

 

以为使用索引就可以修复这个问题是天真的,因为遍历这些边的复杂度上升到了O(log E) + O(k), 这里 E 表示边的总数量。所以选择混合索引(哈希+链表)就可以解决这个难题。我们把所有的边存储在一个巨大的哈希表中, 同时把与顶点 V 相邻的顶点组装成一个双向链表存好。ArangoDB的实现细节可以参阅GitHub知识库。

(译者注:原文的表述并不严谨, 段首提到的 天真的索引 应该指的是基于类似B+Tree的平衡树结构,索引查找本身确实会多一个 O(log E) 的时间复杂度,换成Hash结构的索引,当然可以做到 O(1) 。但是对于范围查找、排序等Hash结构做不到的痼疾原文也是避重就轻,大家懂就行了。)

查询遍历相邻顶点的过程如下:

1. 先找到起始点(这也是图检索的首要原则):在复杂度 O(1) 的哈希索引上检索顶点的Key,这可以保证一个常量时间。

2. 假设起始点有 K 个邻接点,那么遍历它们实际就是遍历链表,所以复杂度是 O(k)。 并且链表遍历只需要运行一次。

3. 时间复杂度从开始的O(1) 加上链表遍历的 O(k),仍旧是 O(k)

 

更新或删除某一个边的过程如下:

1. 如果这个边恰恰位于某个顶点的边链表的第一个,那直接根据Key值找到顶点;

2. 如果不是第一个,那更简单直接的方式,在哈希表里可以直接通过边的Key值来搜索。

3. 因此我们找到边的时间复杂度仅仅是 O(1),剩下的只是更新或者删除即可。

(译者注:就算我现在也完全没琢磨清楚原文作者的意图,第一步的通过顶点查询边是出于什么考虑,直接做第二步不行么?明白的读者可以留言帮我解惑下。)

让看似不可能变成可能的关键,就是因为混合索引兼具的两个特性:

a. 所有的边都在一个巨大哈希表里;

b. 每个顶点的邻接点都以链表的形式存储;

总之,O(k) 作为相邻节点的遍历复杂度,和 O(1) 作为单条边的更新复杂度,都是理论上最好的结果了。

 

鉴于更新操作如此优秀的复杂度,不需要再追究有关常量。尽管如此,遍历的情况下还是有必要考虑的。比如,通过直接指针实现的加速效果到底有多大,至少考虑当这个图上不会有任何更新操作的情况?再如果图形数据量级特别的小(可装配到 L2 缓存中),我们似乎可以说指针的方式会快一些。尽管如此,指针至少也要获取一个缓存行,那么指针方式带来的性能节省可能直接忽略不计。如果没有使用C++或类似的低级语言,这点受益会立即消失掉。

 

另一方面,如果你有一个超大的图,不能完全塞入内存中,那免索引邻接的缺点将变得特别突出。比如你需要看一下图表数据的查询,也就是在大量图数据中做一个模式匹配,索引可能会更快,因为你完全不需要看看具体的这些顶点,只需要索引本身数据,这部分数据远小于完整的节点数据。因此,它对内存和缓存更加友好,而这恰恰是当今的技术最重要的部分。

 

结论

通过选择正确的索引结构,读复杂度和写复杂度达到一个相同的优秀程度是可能的。另外,使用索引将负载压力从主内存中剥离出来,使得算法对于内存和缓存更加友好。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值