Neo4j将边也作为数据库的“一等公民”,将属性图的顶点、边、标签和属性被分开存储在不同文件中。
正是这种将图结构与图上标签 和属性分开存储的策略,使得 Neo4j 具有高效率的图遍历能力.图中给出了 Neo4j 2.2 版本中顶点和边记录的物理存储结构(其他版本可能有变化),其中每个顶点记录占用 15 字节,每个边记录占用 34 字节。
顶点记录的第 0 字节 inUse 是记录使用标志字节,表示该记录是正在使用中还是已经删除并可回收用来装载新记录;第 1 字节~第 4 字节 nextRelId 是与顶点相连的第 1 条边的 id;第 5 字节~第 8 字节 nextPropId 是顶点 的第 1 个属性的 id;第 9 字节~第 13 字节 labels 是指向顶点标签存储的指针,若标签较少会直接存储在此处;第 14 字节 extra 用于存储一些内部使用的标志信息.
边记录第 0 字节 inUse 的含义与顶点记录相同,是表示是否正被数据库使用的标志;第 1 字节~第 4 字节 firstNode 和第 5 字节~第 8 字节 secondNode 分别是该边的起始顶点 id 和终止顶点 id;第 9 字节~第 12 字节 relType 是指向该边的关系类型的指针;第 13 字节~第 16 字节 firstPrevRelId 和第 17 字节~第 20 字节 firstNext RelId 分别为指向起始顶点上前一个和后一个边记录的指针;第 21 字节~第 24 字节 secPrevRelId 和第 25 字节~ 第 28 字节 secNextRelId 分别为指向终止顶点上前一个和后一个边记录的指针;指向前后边记录的 4 个指针形 成了两个“关系双向链”;第 29 字节~第 32 字节 nextPropId 是边上的第 1 个属性的 id;第 33 字节 firstInChain Marker 是表示该边记录是否是“关系链”中第 1 条记录的标志.
Neo4j 能够实现顶点和边快速定位的关键是“定长记录(fixed-size record)”存储方案,以及将具有定长记录的图结构与具有变长记录的属性数据分开存储。
定长记录的好处是可以通过计算偏移值的方式快速定位到存储地址(这里可以类比使用数组下标直接访问内存地址的方式)
例如,一个顶点记录长度是 15 字节,如果要查找 id 为 99 的顶点记录所在位置(id 从 0 开始),则可直接到顶点存储文件第 1485(15X99)个字节处访问(存储文件从第 0 个字节开 始).边记录也是“定长记录”,长度为 34 字节.这样,数据库已知记录 id 可以 O(1)的代价直接计算其存储地址,从而避免了全局索引中 O(logn)的查找代价。
下图展示了 Neo4j 中各种存储文件之间是如何交互的
存储在顶点文件中的顶点 v2 和 v3 均有指针(nextPropId)指向存储在属性文件中各自的 第 1 个属性记录;也有指针(nextRelId)指向存储在边文件中各自的第 1 条边,分别为边 e3 和 e4
若要查找顶点属性,可由顶点找到其第 1 个属性记录,再沿着属性记录的单向链表进行查找
若要查找一个顶点上的边,可由顶点 找到其第 1 条边,再沿着边记录的双向链表进行查找(每个边记录实际上维护着两个双向链表,一个是起始顶点上的边,一个是终止顶点上的边,可以将边记录想象为被起始顶点和终止顶点共同拥有。用双向链表的优势在于,不仅可在查找顶点上的边时进行双向扫描,而且支持在两个顶点间高效率地添加边和删除边.这些操作除了记录字段的读取,就是定长记录地址的计算,均是 O(1)时间的高效率操作)
当找到了所需的边记录后,可由该边进一步找到边上的属性;还可由边记录出发访问该边连接的两个顶点记录(图中虚线箭头)
可见, 正是由于将边作为“一等公民”、将图结构实现为定长记录的存储方案,赋予了 Neo4j 作为原生图数据库的“无索引邻接”特性.
参考文献:
王鑫,邹磊,王朝坤,彭鹏,冯志勇.知识图谱数据管理研究综述.软件学报,2019,30(7):2139-2174. http://www.jos.org.cn/1000-9825/5841.htm