文档数据库是否在重复历史?
虽然多对多关系和joins在关系数据库中经常使用,但文档数据库和NoSQL重新开启了关于如何最好地在数据库中表示这种关系的争论。这个争论比NoSQL老得多——事实上,它可以追溯到最早的计算机化的数据库系统。
20世纪70年代最流行的商业数据处理数据库是IBM的信息管理系统(IMS),最初是为阿波罗空间项目的库存存储而开发的,并于1968年首次发布商业版本。它现在仍然在使用和维护,运行在操作系统为OS/390的IBM大型机上。
IMS的设计使用了一个相当简单的数据模型,称为层次模型,它与文档数据库所使用的JSON模型有一些明显的相似之处。它将所有数据表示为嵌套在记录中的记录树,非常类似于图2-2的JSON结构。
像文档数据库一样,IMS在一对多关系中工作得很好,但是如果是多对多关系就变得相对比较困难,而且它不支持join。开发人员必须决定是否要复制(非正式)数据,或者手动决定将引用从一个记录换到另一个记录。这些20世纪60年代和70年代的问题很像今天的开发者在文档数据库中遇到的问题。
为了解决层次模型的局限性,人们提出了许多解决方案。其中最突出的两个是关系模型(它变成了SQL,并接管了整个世界)和网络模型(最初有大量的追随者,但最终逐渐消失)。这两个阵营之间的“大辩论”贯穿了20世纪70年代的大部分时间。
既然这两个模型正在解决的问题在今天仍然非常重要,那么在今天的讨论中,我们有必要简要地回顾一下这一争论。
网络模型
网络模型由一个名为“数据系统语言会议”(CODASYL)的委员会标准化,并由几个不同的数据库供应商实现;它也被称为CODASYL模型。
CODASYL模型是分层模型的泛化。在分层模型的树结构中,每条记录都有一个父节点;在网络模型中,记录可以有多个父类。例如,“大西雅图地区”区域可能有一个记录,每个在该区域居住的用户都可以链接到该区域。这样就可以对多对一和多对多关系进行建模。
网络模型中记录之间的链接不是外键,而是更像编程语言中的指针(但是存储在磁盘上)。访问记录的惟一方法是沿着这些链接从根记录路径跟踪。这被称为访问路径。
在最简单的情况下,访问路径可能类似于链表的遍历:从列表的头部开始,每次查看一个记录,直到找到您想要的。但是在一个多对多关系的世界中,有几种不同的路径可以导致相同的记录,而使用网络模型的程序员必须跟踪这些不同的访问路径。
CODASYL中的一个查询是通过在数据库中遍历记录列表和跟踪访问路径来移动游标执行的。如果一个记录有多个父母,应用程序代码必须跟踪所有的关系。甚至连CODASYL委员会成员也承认这就像在n维数据空间中导航一样。
尽管手动访问路径选择能够在20世纪70年代最有效地利用有限的硬件功能,但问题是他们编写的查询和更新数据库的代码太复杂,而且不太灵活。既有层次结构又有网络模型,如果您没有找到您想要的数据的路径,那么您的处境就很困难。您可以更改访问路径,但是您必须遍历大量的手写的数据库查询代码并重新编写它来处理新的访问路径。但我们也知道,对应用程序的数据模型进行更改是很困难的。
关系型模型
相反,关系模型所做的是将所有数据公开:一个关系(表)仅仅是一个元组(行)的集合,仅此而已。如果您想查看数据,就没有复杂的嵌套结构,也没有复杂的访问路径。您可以在一个表中读取任何或全部行,选择那些匹配任意条件的行。您可以通过将一些列指定为键,并在这些列上进行匹配,从而读取特定的行。您可以在任何表中插入一个新的行,而不用担心来自其他表的外键关系。
在关系数据库中,查询优化器自动地决定要执行哪部分查询,以及要使用哪些索引。这些选择实际上就是网络模型中所提到的“访问路径”,但最大的不同之处在于,它们是由查询优化器自动生成的,而不是由应用程序开发人员自动生成的,因此我们很少需要考虑它们。
如果您想以新的方式查询数据,您可以只声明一个新的索引,查询将自动使用最合适的索引。您不需要更改查询就能使新索引生效。因此,关系模型使得向应用程序添加新特性变得更加容易。
关系数据库的查询优化器是非常复杂的,而且相关的研究和开发工作已经持续了很多年了。但是关系模型的一个关键见解是:您只需要构建一次查询优化器,然后所有使用该数据库的应用程序都可以从中受益。如果您没有查询优化器,那么为特定的查询编写访问路径要比编写通用的优化器更容易——但是通用的解决方案在长期内会胜出。
与文档数据库的比较
文档数据库在一个方面回归到层次模型中:将嵌套记录(一对多关系,如位置、教育经历和图2-1中的联系信息)存储在它们的父记录中,而不是放在单独的表中。
然而,当涉及到多对一和多对多关系时,关系和文档数据库并不是完全不同的:在这两种情况下,相关的项都由唯一标识符引用,这在关系模型中被称为外键,在文档模型中称为文档引用。这个标识符在读取时通过使用join或连续查询来解析。到目前为止,文档数据库还没有遵循CODASYL的道路。
关系型数据库 VS 文档型数据库
在比较关系数据库和文档数据库时,需要考虑许多不同之处,包括它们的容错特性和并发处理。在这一章中,我们将只关注数据模型的差异。
支持文档数据模型的主要理由是其模式非常灵活,由于局部的性能更好,对于某些应用程序来说,它更接近于应用程序使用的数据结构。关系模型在join、多对一和多对多关系的场景则表现的更好。
哪个数据模型会使更应用程序的代码更简单?
如果应用程序中的数据具有类似文档的结构,那么使用文档模型可能是个好主意。而分解成关系型模型——将类似文档的结构分割成多个表——可能导致繁琐的模式和不必要的复杂的应用程序代码。
但文档模型有一些限制:例如,您不能直接引用文档中的嵌套项,而是您需要说出“用户251的位置列表中的第二项”(非常类似于分层模型中的访问路径)。然而,只要文档不是嵌套太深,这通常不是问题。
根据应用程序的不同,对文档数据库连接的支持可能是个问题,也可能不是问题。例如,在一个分析应用程序中,可能永远不需要多对多的关系,该应用程序使用文档数据库记录在哪个时间段发生的事件。
然而,如果您的应用程序确实使用多对多关系,那么文档模型就变得不那么有吸引力了。通过非标准化来减少对连接的需求是可能的,但是应用程序代码需要做额外的工作来保持非规范化数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟联接,但这也将复杂性转移到应用程序中,而且通常比数据库内部专门代码执行的联接要慢。在这种情况下,使用文档模型可能导致应用程序的代码更加复杂和性能也更加糟糕。
一般来说,不可能确切的说哪种数据模型会使得应用程序代码更简单,这取决于数据项之间存在的关系类型。例如对于高度互联的数据,文档模型很笨拙,关系模型是可以接受的,图形模型是最自然的。
文档模型中的模式灵活性
大多数文档数据库以及关系型数据库中的JSON支持,都不会对文档中的数据强制schema。关系数据库中的XML支持通常带有可选的模式验证。no schema意味着可以将任意的键和值添加到文档中,而在阅读时,客户端无法保证文档可能包含哪些字段。
文档数据库有时被称为schemaless,但这是一种误导,因为读取数据的代码通常假定某种结构。有一个隐式模式,但它不是由数据库强制执行的。更准确的术语是schema-on-read(数据的结构是隐式的,只有在读取数据时才会解释),与schema-on-write(关系数据库的传统方法,模式是显式的,数据库确保所有的书面数据都符合它)。
Schema-on-read类似于在编程语言中动态(运行时)类型检查,而schema-on-write类似于静态(编译时)类型检查。就像静态和动态类型检查的提倡者对他们的相对优点有很大的争论一样,在数据库中实施模式是一个有争议的话题,一般来说没有正确或错误的答案。
在应用程序想要更改其数据格式的情况下,这些方法之间的区别尤其明显。例如,假设您当前将每个用户的全名存储在一个字段中,而您却希望分别存储姓名和姓氏。在文档数据库中,您只需开始编写带有新字段的新文档,并在应用程序中有代码来处理旧文档的读取情况。例如:
if (user && user.name && !user.first_name)
{
// Documents written before Dec 8, 2013 don't have first_name
user.first_name = user.name.split(" ")[0];
}
另一方面,在“静态类型”数据库模式中,您通常会按照以下方式执行迁移:
ALTER TABLE users ADD COLUMN first_name text;
UPDATE users SET first_name = split_part(name, ' ', 1); -- PostgreSQL
UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
模式更改的给人的感觉不好,因为它的速度很慢,需要停机。但这并不是完全正确的:大多数关系数据库系统执行ALTER TABLE语句都在几毫秒内完成。MySQL是一个值得注意的例外——它在ALTER TABLE上复制整个表,这意味着在改变一个大表时可能需要几分钟甚至几个小时的停机时间——尽管存在各种各样的工具来解决这个限制。
在一个大表上运行UPDATE语句在任何数据库上都可能很慢,因为每一行都需要重写。如果这是不可接受的,应用程序就可以将firstname设置为NULL,并在读取时填充它,就像对待文档数据库一样。
如果项目由于某种原因没有相同的结构,那么schema-on-read是有利的(例如数据是异构的)——例如,因为:
• 有许多不同类型的对象,将每种类型的对象放在自己的表中是不切实际的。
• 数据的结构是由您无法控制的外部系统决定的,并且在任何时候都可能发生变化。
在这种情况下,有固定的schema可能会比没有伤害更大,而schemaless的文档可能是一个更自然的数据模型。但是,在所有记录都具有相同结构的情况下,schema是记录和执行该结构的有用机制。
查询数据区域
文档通常存储为单个连续字符串,编码为JSON、XML或其二进制变体(如MongoDB的BSON)。如果您的应用程序经常需要访问整个文档(例如,在web页面上呈现它),那么这个存储区域就有一个性能优势。如果数据跨多个表进行分割,如图2-1,则需要多个索引查找来检索所有数据,这可能需要更多的磁盘查找和花费更多的时间。
如果您同时需要大量的文档,那么区域优势只适用于此。数据库通常需要加载整个文档,即使您只访问其中的一小部分,这在大型文档中可能是浪费。在对文档进行更新时,整个文档通常需要重写——只有不改变文档编码大小的修改才能很容易地执行。由于这些原因,通常建议您将文档保持在相当小的范围内,尽量避免增加文档大小的写操作。这些性能限制显著地减少了文档数据库有效的场景场景。
值得指出的是,将相关数据分组在一起的想法并不局限于文档模型。例如,Google的Spanner在关系数据模型中提供了相同的位置属性,允许schema声明表的行应该在父表中交错(嵌套)。Oracle允许使用相同的功能,使用一个称为多表索引集群表的特性。Bigtable数据模型中的column-family概念(在Cassandra和HBase中使用)与管理区域的目的类似。
文档和关系数据库的聚合
自2000年代中期以来,大多数关系数据库系统(除了MySQL之外)都支持XML。这包括对XML文档进行局部修改的功能,以及在XML文档中索引和查询的能力,这使得应用程序可以使用与使用文档数据库时所做的非常相似的数据模型。
PostgreSQL自版本9.3以来,MySQL版本为5.7,而自版本10.5以来的IBM DB2也有类似的对JSON文档的支持。考虑到JSON对于web api的流行,其他关系数据库很可能会步其后尘,添加JSON支持。
在文档数据库方面,RethinkDB支持类关系型的连接,一些MongoDB驱动程序自动解析数据库引用(有效地执行客户端联接,尽管这可能比数据库中执行的连接要慢,因为它需要额外的网络往返,并且没有优化)。
随着时间的推移,关系和文档数据库似乎变得越来越相似,这是一件好事:数据模型是互补的。如果一个数据库能够处理类似于文档的数据,并且在其上执行关系查询,应用程序可以使用最适合其需求的特性组合。
关系和文档模型的混合是数据库未来的一个很好的途径。