本章目标: 比较多种不同的数据模型和查询语言,讨论各自的适用场景。
第2章 数据模型与查询语言数据模型可能是开发软件最重要的部分,它们不仅对软件的编写方式,而且还对如何思考待解决的问题都有深远的影响。大多数应用程序是通过一层一层叠加数据模型来构建的。每一层都面临的关键问题是:如何将其用下一层来表示?
早期的其他数据库迫使应用开发人员考虑数据的内部表示。关系模型的目标就是将实现细节隐藏在更简洁的接口后面。
进入21世纪,NoSQL成为推翻关系模式主导地位的有一个竞争者。“NoSQL”这个名字是不恰当的,因为它其实并不代表具体的某些技术,它最初只是作为一个吸引人眼球的Twitter标签频频出现在2009年的开源、分布式以及非关系数据库的见面会上。尽管如此,这个称呼还是让人有所触动,并迅速传遍了网络创业社区。现在很多新兴的数据库系统总是会打上NoSQL的标签,而其含义也已经被逆向重新解释为“不仅仅是SQL”。
不同的应用程序有着不同的需求,某个用例的最佳的技术选择未必合适另一个用例。因此,在可预见的将来,关系数据库可能仍将继续与各种非关系数据存储在一起使用,这种思路有时也被称为混合持久化。
现在大多数应用开发都采用面向对象的编程语言,由于兼容性问题,普遍对SQL数据模型存在抱怨:如果数据存储在关系表中,那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。 模型之间的脱离有时被称为阻抗失谐。ActiveRecord和Hibernate这样的对象-关系映射(ORM)框架则减少了此转换层所需的样板代码量,但是他们并不能完全隐藏两个模型之间的差异。
为数据结构选取最适应的数据模型是很重要的。对于像简历这样的数据结构,它主要是一个自包含的文档(document),因此用JSON表示非常合适。用户简历到用户的职位、教育历史和联系信息的一对多关系,意味着数据存在树状结构,JSON表示将该树结构显示化。
JSON表示比多表模式具有更好的局部性。如果要在关系模式中读取一份简历,那么要么执行多个查询(通过user_id查询每个表),要么在users表及其从属表之间执行混乱的多路联结。而对于JSON表示方法,所有的相关信息都在一个地方,一次查询就够了。
无论是存储ID还是文本字符串,都涉及内容重复的问题。当使用ID时,对人类有意义的信息(例如慈善这个词)只存储在一个地方,引用它的所有内容都使用ID(ID只在数据库中有意义)。当直接存储文本时,则使用它的每条记录中都保存了一份这样可读信息。简单来讲就是一个事物到底要不要单独作为一个实体抽象出来。 使用ID的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使ID标识的信息发生了变化,它也可以保持不变。任何对人类有意义的东西都可能在将来某个时刻发生变更。如果这些信息被复制,那么所有的冗余副本也都需要更新。这会导致更多写入开销,并且存在数据不一致的风险(信息的一些副本被更新,而其他副本未更新)。消除这种重复正是数据库规范化的核心思想。
这种数据规范化需要表达多对一的关系,并不是很适合文档模型。关系数据库支持联结操作,而文档数据库中,一对多的树状结构不需要联结,支持联结通常很弱。如果数据库本身不支持联结,则必须在应用应用程序代码中,通过对数据库进行多次查询来模拟联结。
即使应用程序的初始版本非常适合采用无联结的文档模型,但随着应用支持越来越多的功能,数据也变得更加一体化。
层次模型与文档数据库使用的JSON模型有一些显著的相似之处。它将所有数据标识为嵌套在记录中的记录(树),与JSON结构非常相似。
和文档数据库类似,IMS可以很好地支持一对多关系,但是它支持多对多关系则有些困难,而且不支持联结。开发人员必须决定是复制(反规范化)多份数据,还是手动解析记录之间的引用。20世纪六七十年代的这些问题与开发人员今天遇到的文档数据库的问题非常相似。
CODASYL模型是层次数据库的推广。在层次模型的树结构中,每个记录只有一个父节点;而在网络模型中,一个记录可能有多个父节点。
在网络模型中,记录之间的链接不是外键,更像是编程语言中的指针(会存储在磁盘上)。访问记录的唯一方法是选择一条始于根记录的路径,并沿着相关链接依次访问。这条链接链条也因此被称为访问路径。最大的问题在于它们使查询和更新数据库变得异常复杂而没有灵活性。无论是层次模型还是网络模型,如果脱离数据的访问路径,那么将寸步难行。
在关系数据库中,查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。这些选择实际上等价于“访问路径”,但最大的区别在于它们时由查询优化器自动生成的,而不是由应用开发人员所维护,因此不用过多地考虑它们。关系模型使得应用程序添加新功能变得非常容易。
关系数据库的查询优化器称得上是一个复杂的怪兽,研究开发人员多年来持续投入,花费巨大。不管怎样,关系模型的一个核心要点是:只需构建一次查询优化器,然后使用该数据库的所有应用程序都可以从中受益。如果没有查询优化器,那么为特定查询手动编写访问路径比编写通用优化器更容易,但从长久来看,通用解决方案更胜一筹。
文档数据库是某种方式的层次模型:即在其父记录中保存了嵌套记录(一对多关系),而不是存储在单独的表中。
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项都由唯一的标识符引用,该标识符在关系模式中被称为外键,在文档模型中被称为文档引用。标识符可以查询时通过联结操作或相关后续查询来解析。迄今为止,文档数据库并未遵循CODASYL标准。
比较两者时间,需要考虑很多方面的差异,包括容错性和并发处理。
支持文档数据模型的主要论点是模式灵活性,由于局部性而带来较好的性能,对于某些应用来说,它更接近与应用程序所使用的数据结构。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上,与文档模型抗衡。
通常无法一概而论哪种数据模型的应用代码更简单。这主要取决于数据项之间的关系类型。对于高度关联的数据,文档类型不太适合,关系模型可以胜任,而图模型则是最为自然的。
大多数文档数据库,以及关系数据库中的JSON支持,都不会对文档中的数据强制执行任何模式。关系数据库中的XML通常支持带有可选的模式验证功能。没有模式意味着可以将任意的键-值添加到文档中,并且在读取时,客户端无法保证文档可能包含哪些字段。
文档数据库有时被称为无模式,但这具有误导性,因为读数据的代码通常采用某种结构因而存在某些隐形模式,而不是由数据库强制执行。更准确的术语应该是读时模式(数据的结构是隐式的,只有在读取时才解释),与写时模式(关系数据库的一种传统方法,模式是显示的,并且数据库确保数据写入时都必须遵循)相对应。
当应用程序需要改变数据格式时,这些方法之间的差异就变得尤其明显。如果集合中的项由于某种原因(例如数据异构),并不都有相同的结构,例如:
在这些情况下,模式带来的损害大于它所能提供的帮助,无模式文档可能是更自然的数据模型。但是,当所有记录都有相同结构时,模式则是记录和确保这种结构的有效机制。
局部性优势仅适用需要同时访问文档大部分内容的场景。只访问加载了整个文档的一小部分,修改文档时增加了文档的大小,这些性能方面的不利因素大大限制了文档数据库的适用场景。
值得指出的是,将相关数据归为一组的局部性想法并不仅见于文档模型。例如,Google的Spanner数据库在关系数据模型中提供了相同的局部性,支持模式声明某些表的行应该在父表内交错(嵌套)。Oracle支持类似的操作,称为“多表索引集群表”特性,Bigtable数据模型(用于Cassandra和HBase)中的列族概念类似。
随着时间的推移,似乎关系数据库与文档数据库变得越来越相近,或许这是一件好事:数据模型可以相互补充。如果数据库能够很好处理文档类数据,还能对其执行关系查询,那么应用程序可以使用最符合其需求的功能的组合。
融合关系模型与文档模型是未来数据库发展的一条很好的途径。
当关系模型是初被引入时,就包含了查询数据的新方法:SQL是一种声明式查询语言,而IMS和CODASYL则是命令式。这种差别意味着什么呢?
命令式语言告诉计算机以特定顺序执行某些操作。而对于声明式查询语言(如SQL或关系代数),则只需指定所需的数据模式,结果需满足什么条件,以及如何转换数据(例如,排序、分类和聚合),而不需指明如何实现这一目标。数据库系统的查询优化器会决定采用哪些索引和联结,以及用何种顺序来执行查询的各个语句。
声明式查询语言很有吸引力,它比命令式API更加简洁和容易使用。但更重要的是,它对外隐藏了数据库引擎的很多实现细节,这样数据库系统能够在不改变查询语句的情况下提高性能。
最后,声明式语言通常适合并行执行。现在CPU主要通过增加核,而不是通过比之前更高的时钟频率来提升速度。而命令式代码由于指定了特定的执行顺序,很难在多核和多台机器上并行化。声明式语言则对于并行执行更为友好,它们仅指定了结果所满足的模式,而不指定如何得到结果的具体算法。所以如果可以的话,数据库都倾向于采用并行方式实现查询语言。
声明式查询语言的有点不仅限于数据库。CSS的选择器与JS的对象模型形成鲜明的对比。对于Web浏览器的例子,适用声明式CSS样式表比用Javascript命令式地操作样式好得多。类似地,在数据库中,像SQL这样的声明式查询语言比命令式查询APIs要好得多(例如性能提升和颜色擦除等方面)。
MapReduce是一种编程模型,用于在许多机器上批量处理海量数据,兴起于Google。一些NoSQL存储系统(例如MongoDB和CouchDB)支持有限的MapReduce方式在大量文档上执行只读查询。
MapReduce既不是声明式查询语言,也不是一个完全命令式的查询API,而是介于两者之间:查询的逻辑用代码片段来表示,这些代码片段可以被处理框架重复地调用。它主要基于许多函数式编程语言中的map(也称为collect)和reduce(也称为fold或inject)函数。
MapReduce是一个相当底层的编程模型,用于在计算集群上分布执行。而SQL这样的更高层次的查询语言可以通过一些MapReduce操作pipeline来实现,当然也有很多SQL的分布式实现并不借助MapReduce。请注意,SQL并没有任何限制规定它只能在单个机器上运行,而MapReduce也并非垄断了分布式查询。
一开始,我以为图的顶点只能表示相同类型的事物。然而,图并不局限于这样的同构数据,图更为强大的用途在于,提供了单个数据存储区中保存完全不同类型对象的一致性方式。
有多种不同但相关的方法可以构建和查询图中的数据。接下来将讨论属性图模型和三元存储模型,以及介绍三种声明式图查询语言,还有命令式图查询语言,以及图处理框架。
图有利于演化:向应用程序添加功能时,图可以容易地扩展以适应数据结构的不断变化。
可以将图存储看作由两个关系表组成,一个用于顶点,另一个用于边。为每个边存储头部和尾部顶点,如果想要顶点的入边或出边的集合,可以分别通过head_vertex或tail_vertex来查询edges表。
Cypher是一种用于属性图的声明式查询语言,最早为Neo4j图形数据库而创建。
对于一个具体的需求,往往存在多种实现方式。对于声明式查询语言,通常在编写查询语句时,不需要指定执行细节:查询优化器会自动选择效率最高的执行策略,因此开发者可以专注于应用的其他部分。
可以采用关系数据库表示图数据。如果把图数据放在关系结构中,是否意味着也可以支持SQL查询呢?
答案是肯定的,但存在一些困难。在关系数据库中,通常会预先知道查询中需要哪些join操作。而对于图查询,在找到要查找的定点之前,可能需要遍历数量未知的边,也就是说,join操作数量并不是预先确定的。
Cypher可以用:WITHIN*O…非常简洁地表达语义:它表示“沿着一个WITHIN边,遍历零次或多次”,就像正则表达式中的*运算符(表示匹配零次或多次)那样。SQL:1999标准以后,查询过程中这种可变的遍历路径可以使用称为递归公用表表达式(即WITH RECURSIVE语法)来表示。但与Cypher相比,语法仍显得非常笨拙。
如果相同的查询可以用一种查询语言写4行代码完成,而另一种查询语言则需要29行代码,这足以说明不同的数据模型适用于不同的场景。因此,选择适合应用程序的数据模型非常重要。
三元存储模式几乎等同于属性图模型,只是使用不同的名词描述了相同的思想。尽管如此,考虑到有多种针对三元存储的工具和语言,它们可能是构建应用程序宝贵的补充。
三元组的主体相当于图中的顶点。而客体则是以下两种之一:
如果关于三元存储的信息,很可能会被卷入关于语义网大量文章漩涡之中。三元存储数据模型其实完全独立于语义网,例如,Datomic是一个三元存储,它与语义网并没有任何关系。
语义网从本质上讲源于一个简单而合理的想法:网站通常将信息以文字和图片方式发布给人类阅读,那为什么不把信息发布为机器可以阅读的格式给计算机阅读呢? 资源描述框架(Resource Description Framework,RDF就是这样一种机制),它让不同网站以一致的格式发布数据,这样来自不同网站的数据自动合并成一个数据网络,一种互联网级别包含所有数据的数据库。
使用的Turtle语言代表了RDF数据的人类可读格式。因为旨在为全网数据交换而设计,RDF存在一些特殊的约定。三元组的主体、谓语和客体通常是URI。例如谓语可能是URI,这种设计背后的原因是,它假设你的数据需要和其他人的数据相结合,万一不同人给单词within或者lives_in附加了不同的含义,采用URI则可以避免冲突。从RDF的角度来看,其不一定需要解析出特定的内容,它更多的只是一个命名空间。
SPARQL是一种采用RDF数据模型的三元存储查询语言,它比Cypher更早,并且由于Cypher的模式匹配是借用的SPARQL的,所以二者看起来非常相似。执行同样的查询,SPARQL比Cypher更加简洁。
Datalog是比SPARQL或Cypher更为古老的语言,在20世纪80年代被学者广泛研究。虽然在软件工程师中知名度较低,但它为以后的查询语言奠定了基础,因此它非常重要。实践中有几个数据库系统采用了Datalog。例如它是Datomic系统的查询语言,而Cascalog是用于查询Hadoop大数据集的Datalog实现。
Datalog的数据类型类似于三元存储模式,但更为通用一些。它采用“谓词(主体,客体)”的表达方式而不是三元组(主体,谓语,客体)。Cypher和SPARQL通过类似SELECT一次完成查询,而Datalog则每次实现一块。我们定义了告诉数据库关于新谓语的规则,例如两个新的谓语within_recursive和migrated。这些谓语并不是存储在数据库中的三元组,而是从数据或其他规则派生而来。规则可以引用其他规则,就像函数可以调用其他函数或者递归调用自己一样。像这样,复杂的查询可以通过每次完成一小块而逐步构建。
Datalog方法需要采取与其他查询语言略有不同的思维方式,但它非常强大,特别是规则可以在不同的查询中组合和重用。对于简单的一次性查询来说,这或许不太方便,但是如果数据非常复杂,处理起来会更加游刃有余。
历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不太适合关系模型。新的非关系“NoSQL”数据存储在两个主要方向上存在分歧:
所有这三种模型(文档模型、关系模型和图模型),如今都有广泛的使用,并且在各自的目标领域都足够优秀。我们观察到,一个模型可以用另一个模型来模拟。例如,图数据可以在关系数据库中表示,虽然处理起来比较笨拙。这就是为什么不同的系统用于不同的目的,而不是一个万能的解决方案。
文档数据库和图数据库都一个共同点,那就是它们通常不会对存储的数据强加某个模式,这可以使应用程序更容易适应不断变化的需求。但是,应用程序很可能仍然假定数据具有一定的结构,只不过是模式是显示(写时强制)还是隐式(读时处理)的问题。