技术编程|像图一样的数据模型


前面我们已经看到 , 多对多关系是不同数据模型之间的重要区别特征 。 如果您的应用程序主要具有一对多关系(树结构数据)或记录之间没有关系 , 则文档模型是合适的 。
但是 , 如果您的数据中多对多关系很常见怎么办?关系模型可以处理多对多关系的简单情况 , 但是随着数据中的连接变得越来越复杂 , 更常规的做法是开始将数据建模为图 。
图由两种对象组成:顶点vertices(也称为节点或实体)和边edges(也称为关系或弧) 。 可以将多种数据建模为图 。 典型示例包括:
社交图:顶点是人 , 边表示哪些人彼此认识 。
网络图:顶点是网页 , 边表示指向其他页面的HTML链接 。
公路或铁路网:顶点是交汇点 , 边表示它们之间的道路或铁路线 。
众所周知的算法可以在这些图上运行:例如 , 汽车导航系统搜索道路网络中两点之间的最短路径 , 而PageRank可以在网络图上使用以确定网页的受欢迎程度并由此在搜索结果中确定其排名 。
在刚才给的例子中 , 图中的所有顶点代表了相同类型的事物(分别是人 , 网页 , 或是道路交汇点) 。 但是 , 图并不仅限于此类同质数据:图相当强有力的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方法 。 例如 , Facebook维护了具有多个不同类型的顶点和边缘的单个图:顶点代表人员 , 位置 , 事件 , 签到和用户发表的评论;边表示哪些人是彼此的朋友 , 哪些签到发生在哪个位置 , 谁在哪个帖子上发表评论 , 谁参加了哪个活动 , 等等 。
接下来 , 我们会以下图为例展开讨论 。 它来自社交网络或是一个家谱数据库:它显示了两个人 , 分别是爱达荷州Idaho的Lucy和法国博恩Beaune的Alain 。 他们已婚并居住在伦敦 。

技术编程|像图一样的数据模型
本文插图

有几种不同但相关的结构化和查询图中数据的方式 。 之后 , 我们将讨论属性图模型(由Neo4j , Titan和InfiniteGraph实现)和Tripe-Stores存储模型(由Datomic , AllegroGraph等实现) 。 我们将研究图的三种声明性查询语言:Cypher , SPARQL和Datalog 。 除了这些 , 还有命令式的图查询语言 , 例如Gremlin和图处理框架 , 例如Pregel 。 属性图Property Graph
在属性图中 , 每个顶点由下列组成:
一个独一无二的标识符
一组向外(指向其他顶点)的边
一组向内(指向自己的)的边
属性集合(键值对)
每条边由下列组成:
一个独一无二的标识符
边开始的顶点(尾部顶点tail vertex)
边结束的顶点(头部顶点head vertes)
一个描述两个顶点间关系的标签
属性集合(键值队)
你可以认为图是由两张关系型表组成:一个定点的表 , 一个是边的表 , 如下所示:

技术编程|像图一样的数据模型
本文插图

基于PostgreSQL
头顶点和尾顶点存储于每一个边中;如果你想要一个顶点的一组入边和出边 , 你可以分别通过边表中的head_vertex或是tail_vertex查询 。
这个模型中的一些重要的注意事项是:
任何顶点都有一条边与其他顶点相连 。 对于关联没有任何的限制 。
给定任何顶点 , 您都可以有效地找到其传入和传出的顶点边 , 从而遍历图 - 即沿着一条通过一连串顶点的路径 - 向前和向后都行(这就是为什么在上图中建立索引) 。
通过使用不同关系的不同标签 , 你可以好几种不同类系的信息存储于单个图中 , 从而维护一个干净的数据模型
这些功能为图(如下图)提供了极大的数据建模灵活性 。
该图显示了一些难以用传统的关系模式表达的东西 , 例如不同国家的不同类型的区域结构(法国拥有departments和regions , 而美国则具有countries和states) , 诸如一个country内有另一个country的奇葩的历史问题 , 以及不同的数据粒度(Lucy的当前住所被指定为city , 而她的出生地仅在state一级被指定) 。
您可以想象将图扩展到还包括有关露西和阿兰或其他人的许多其他的方方面面面 。 例如 , 您可以使用它来表示他们具有的任何食物过敏(通过为每个过敏原引入一个顶点 , 并在人和过敏原之间引入一条边来表示过敏) , 并将这些过敏原与一组顶点相连 , 从而显示出哪些食物中含有哪些物质 。 然后你可以写一个查询到找出每个人食用的安全食品 。 图形具有很好的可扩展性:在向应用程序添加功能时 , 可以轻松扩展图形以适应应用程序数据结构中的更改 。 Cypher查询语言
Cypher是一种属性图的声明式的查询语言 , 是为Neo4j图数据库创建的(它以电影《黑客帝国》中的角色命名 , 而不是与密码学中的密码有关) 。
下列显示了Cypher查询 , 该查询将上图的左手部分插入到图形数据库中 。 图的其余部分可以类似地添加 , 为便于阅读 , 将其省略 。 每个顶点都有一个符号名称 , 例如USA或Idaho , 查询的其他部分可以使用这些名称 , 通过箭头符号在顶点之间创建边:(Idaho)-[:WITHIN]->(USA)创建了标签为WITHIN的边 , 爱达荷州为尾节点 , USA为头节点 。

技术编程|像图一样的数据模型
本文插图

当所有的顶点和边都加入到数据库中后 , 我们开始问一些有趣的问题 。 比如说 , 查找所有从美国移民到欧洲的人的名字 。 为了更精确的描述 , 这里我们是想要找出所有:
有BORN_IN标签的指向美国境内位置的边
有LIVES_IN标签的指向欧洲境内位置的边
的顶点 , 然后返回这些顶点的每一个的name属性 。
下图展示了用Cypher是怎么表示这个查询的 。 在MATCH子句中使用相同的箭头符号在图中查找模式:(person)-[:BORN_IN]->() , 匹配由标签为BORN_IN的边相关的任何两个顶点 。 该边的尾顶点绑定到变量person , 而头顶点是未命名 。

技术编程|像图一样的数据模型
本文插图

这个查询会按照如下读取:
找出任何一个满足如下两个条件的person顶点:
顶点person有一个往外的 , 指向某个顶点的 , 带有BORN_IN标签的边 。 从那个顶点开始 , 你可以沿着一连串 , 带有WITHIN标签的边 , 最终到达一个类型为Location , 且其name属性为“Unites States"的顶点 。
同一个person顶点 , 有一个往外的带有LIVES_IN标签的边 。 沿着这个边 , 会有一连串的带有WITHIN标签的向外的边 , 并且最终到达一个类型为Location , 且其name属性为“Europe"的顶点 。
对于每一个顶点 , 返回name属性
有好几种可能的方式来执行这个查询 。 这里给描述建议是从扫描所有数据库中的人 , 检索每个人的出生地和居住地 , 然后返回满足条件的人 。
但类似的 , 你也可以从两个Location顶点开始 , 往后查询 。 如果有一个name属性的索引 , 你可能会很高效的找出两个代表US和Europe的顶点 。 然后你可以找出分别通过所有带有往内标签WITHIN的边 , 找出位于美国和欧洲境内的所有位置(包括stats、regions、cities等等) 。 最终 。 你就可以通过一条带有BOIN_IN或LIVES_IN标签的往内(指向一个Location顶点)的边查询到这个人了 。
作为一个典型的声明式语言 , 你在书写查询时不用指定上述的执行细节:查询优化器会自动选择最有效的方式 , 而你可以继续写您的应用程序的其余部分 。 SQL中的图查询
之前我们说过图数据可以用关系行数据库表示 , 但如果我们这么做了 , 我们可以用SQL来查询吗?
答案是可以的 , 但有一些困难 。 在关系型数据库中 , 你通常需要提前知道你要关联哪些你需要的表 。 而在一个图查询中 , 您可能需要遍历可变数量的边 , 才能找到所需的顶点 , 也就是说 , 关联的数量不是预先能确定的 。 在我们的示例中 , 这发生在Cypher查询中 , 发生在()-[:WITHIN*0...]->()里 。 一个带有person顶点的 , 有LIVES_IN标签的边可能指向任何一个位置:street、city、district、region、state等等 。 一个city可能与一个region是WITHIN的关系 , 一个region WITHIN 一个state , 一个state WITHIN 一个country等等 。 带有LIVES_IN标签的边可能直接指向你搜寻位置的顶点 , 或者也有可能经过几个层级才能到达 。
在Cypher中 , :WITHIN*0.. 非常简洁地表达了这一事实:它的意思是“沿着WITHIN边 , 零次或多次 。 ”就像正则表达式中的*运算符 。
从SQL:1999开始 , 通过使用名为“recursive common table expressions”来代表在查询中长度可变的遍历路径 。 下图展示了“查找所有从美国移民到欧洲的人的名字”的相同查询 。 然而 , 相比Cypher显得非常笨拙 。

技术编程|像图一样的数据模型
本文插图

首先找出name属性的值是“United States”的顶点 , 并且设置它为in_usa顶点组中第一个元素
沿着所有往内指向in_usa顶点组中顶点 , 带有within标签的边 , 找出所有的顶点 , 把它们都加入到同一个组中 , 直到所有的within边都被访问过
以相同的方式建立in_europe顶点组
对于每个在in_usa组中的顶点 , 沿着往内指向的born_in边 , 找出所有出现在同一位置的人的顶点
同理与in_europe
最后 , 把出生在美国的人的组和居住在欧洲的人的组相交
如果同一查询可以用一种查询语言用4行编写 , 而用另一种查询语言需要29行编写 , 则仅表明不同的数据模型旨在满足不同的用例 。 选择适合您的应用程序的数据模型很重要 。 Triple-Stores 和 SPARQL
Triple-Stores模型是最等效于属性图模型的 , 使用不同的词来描述相同的想法 。 尽管如此 , 还是值得讨论 , 因为对于Triple-Stores来说 , 有多种工具和语言可以为构建应用程序的工具箱提供有价值的补充 。
在Triple-Stores中 , 所有信息都以非常简单的三部分语句的形式存储:(subject , predicate , object) 。 例如 , 在三元组(Jim , likes , bananas)中 , Jim是subject , likes是predicate , 而bananas是obejct 。
三元组中的subject等效于图中的顶点 。 而object是下面两项中的一项:
基本数据类型中的值 , 例如字符串或数字 。 在这种情况下 , 三元组的predicate和object等效于subject顶点上属性的键和值 。 例如 , (lucy , age , 33)就像一个具有属性{“ age”:33}的顶点lucy 。
图中的另一个顶点 。 在这种情况下 , predicate是图中的一条边 , subject是尾部顶点 , 而object是头部顶点 。 例如 , 在(lucy , marriedTo , alain)中 , subejct lucy和obejctalain都是顶点 , 并且predicate marriedTo是连接他们的边的标签 。
下图展示了之前用Cypher所表述的插入数据相同的意思 , 以三元组的形式写成一种名为Turtle(Notation3(N3)的子集)的格式 。

技术编程|像图一样的数据模型
本文插图

在这个例子中 , 图的顶点被写作_:someName 。 当predicate代表边时 , object是一个顶点 , 诸如_:idaho :within _:usa
当predicate是一个属性时 , object就是一串字符串文字 , 比如说:_:usa :name "Unisted States"
一遍又一遍地重复相同的object是相当重复的 , 但是幸运的是 , 您可以使用分号对同一subject做好几件事 。 这使得Turtle格式相当不错并且可读性强 , 如下图:

技术编程|像图一样的数据模型
本文插图

The semantic web语义网
如果您阅读有关Triple-Stores的更多信息 , 您可能会被大量关于语义网semantic web的文章所吸引 。 Triple-Stores数据模型完全独立于语义网-例如 , Datomic是一个Triple-Stores , 它不声称与它有任何关系 。但是由于在很多人的脑海中两者之间有着如此紧密的联系 , 因此我们应该简要地讨论一下 。
语义网从根本上讲是一个简单而合理的想法:网站已经将信息发布为文本和图片供人类阅读 , 那么为什么不将信息也发布为机器可读数据供计算机阅读呢?资源描述框架(Resource Description Framework , RDF)旨在作为一种机制 , 使不同的网站以一致的格式发布数据 , 从而允许将来自不同网站的数据自动组合到数据网络a web of data中 , 这是Internet范围内的“所有事物的数据库”的一类 。
不幸的是 , 语义网在2000年代初期被过度炒作 , 但到目前为止 , 它并没有表现出在实践中被实现的任何迹象 , 这使许多人对此表示怀疑 。 它还遭受了令人眼花乱的首字母缩略词 , 过于复杂的标准提案和自负 。
但是 , 如果您回过头来看这些失败 , 语义Web项目中还是有很多出色工作的 。 即使您没有兴趣在语义Web上发布RDF数据 , 三元组也可能是应用程序的良好内部数据模型 。 RDF数据模型
上文中的Turtle语言是一种人类可读的RDF数据 。 有时RDF被写成XML的格式 , 更详细的描述了上文中Turtle语言所做的事情 , 如下图:

技术编程|像图一样的数据模型
本文插图

由于看起来更简单 , Turtle/N3是完美的 , 像Apache Jena这样的工具能够自动在必要的情况下转换不同的RDF格式 。
由于RDF专为整个Internet的数据交换而设计 , 因此存在一些怪癖 。 三元组的主题 , 谓词和宾语通常是URI 。 subject , predicate , 和object的三元组通常都是URI 。 比方说 , predicare可能是诸如

这样的URI , 而不是像WTHIN或LIVES_IN这样 。 这样设计的理由是这样可以和他人的数据合并 , 并且 , 如果他们对within或libves_in添加了不同的意思 , 你不会因此而冲突 , 因为他们的pridicate实际上是这样的:


从RDF的角度上来看 , URL
不是必须去解决什么问题的 , 他就是个命名空间 。 幸运的是 , 您只需在文件顶部指定一次该前缀 , 然后就不用管它了 。 SPARQL查询语言
SPARQL是像triple-stores这样用RDF数据模型的数据库的查询语言 。 它是SPARAL Protocol and RDF Query Language的缩写 , 读作“sparkle” 。 它早于Cypher , 并且由于Cypher的模式匹配是从SPARQL借来的 , 因此它们看起来非常相似 。
与之前相同的查询(查找从美国移居到欧洲的人)在SPARQL中比在Cypher中更为简洁 , 如下图:

技术编程|像图一样的数据模型
本文插图

结构非常相似 。 下面的两句表达式是相等的:

技术编程|像图一样的数据模型
本文插图

因为RDF并不区分属性和边 , 把它们都看作predicate , 因此你可以使用相同的语法去匹配属性 。 在下面的表达式中 , 变量usa被绑定到一个有name属性且属性值是字符串“United States”的顶点:

技术编程|像图一样的数据模型
本文插图

SPARQL是一种不错的查询语言 , 即使语义网从未发生过 , 它也可以成为应用程序在内部使用的强大工具 。
图数据库与网络模型的比较
之前我们讨论过CODASYL和关系行模型是怎样在IMS中竞争去解决多对多关系的问题的 。 第一眼看上去 , CODASYL的网络模型和图模型很相似 。 那么 , 图数据库是CODASYL的第二次变相吗?
不 , 它们有以下几点的不同:
- 在CODASYL中 , 数据库有专门的模式去指定需要的记录怎么嵌入到其他的记录中 。 在图数据库中 , 没有这样的限制:任何顶点都可以和任何其他的顶点有一条边 。 这为应用程序提供了更大的灵活性 , 以适应不断变化的需求 。
- 在CODASYL中 , 到达特定记录的唯一方法是对这条记录的访问路径中的一条 。 在图数据库中 , 你可以直接引用顶点独一无二的ID , 或是用一个特定值作为索引去找到顶点 。
- 在CODASYL中 , 记录的子记录是一个有序的组 , 所以数据库不得不维护这个序列并且往数据库插入新数据的应用必须注意新记录在这个组中的位置 。 而在图数据库中 , 顶点和边是没有顺序的(当然 , 你可以根据查询的结果排序)
- 在CODASYL中 , 所有的查询是命令式的 , 很难去编写并且因为模式的改变而经常要去重写 。 在图数据库中 , 如果你想的话 , 你可以以命令式的代码写下你的遍历 , 但通常图数据库也支持高级别的声明式语言 , 诸如Cypher或SPARQL 。 基石:Datalog
Datalog是比SPARAL或Cypher更老的语言 , 在1980年代 , 被学术界广泛的研究 。 在软件工程师中很少被知晓 , 尽管不是很重要 , 但他也为后来的建立的查询语言奠定了基础 。
实际上 , Datalog用于一些数据系统:例如 , 它是Datomic的查询语言 , 而Cascalog是用于在Hadoop中查询大型数据集的Datalog实现 。
Datalog的数据模型有点类似于triple-stroes模型 。 但不是将三元组写为(subject, predicate,object) , 而是将其写为predicate(subject, object) , 如下图:

技术编程|像图一样的数据模型
本文插图

现在我们已经定义了数据 , 我们可以编写与之前相同的查询 , 如下图所示 。 它看起来与Cypher或SPARQL中的等效项有所不同 , 但请不要失望 。 Datalog是Prolog的子集 , 如果您学习过计算机科学 , 则可能会见过 。

技术编程|像图一样的数据模型
本文插图

Cypher和SPARQL通过SELECT跳入 , 但是Datalog一次只迈出了一小步 。 我们定义了一些规则来告诉数据库新的predicate:在这里 , 我们定义了两个新的predicate , within_recursive和migrated 。 这些predicate不是存储在数据库中的三元组 , 而是取自数据或其他规则 。 规则可以引用其他规则 , 就像函数可以调用其他函数或递归调用自己一样 。 这样 , 可以一次构建一小块复杂的查询 。
【技术编程|像图一样的数据模型】在规则中 , 以大写字母开头的单词是变量 , 并且像Cypher和SPARQL中一样对predicate进行匹配 。 例如 , name(Location , Name)通过Location = namercia和Name="North America"这样的变量绑定来匹配name(namerica, 'North America) 。


    推荐阅读