diff --git a/ch2.md b/ch2.md index 924b9128..69904e5c 100644 --- a/ch2.md +++ b/ch2.md @@ -2,80 +2,81 @@ ![](img/ch2.png) -> 语言的极限即世界的极限 +> 语言的边界就是思想的边界。 > -> —— 路德维奇·维特根斯坦, 《逻辑哲学》(1922) +> —— 路德维奇·维特根斯坦,《逻辑哲学》(1922) > ------------------- [TOC] -数据模型可能是软件开发中最重要的部分了,因为它有着深远的影响:不仅影响软件的编写方式,而且会影响我们的**解题思路**。 +数据模型可能是软件开发中最重要的部分了,因为它们的影响如此深远:不仅仅影响着软件的编写方式,而且影响着我们的**解题思路**。 -多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来表示的?例如: +多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来**表示**的?例如: -1. 作为一名应用开发人员,你对现实世界进行观察(包括人员,组织,货物,行为,资金流向,传感器等),并使用对象或数据结构建模,提供操纵这些数据结构的API。这些结构通常是应用特定的。 -2. 当你想要存储这些存储这些数据结构时,你可以使用通用数据模型来表示它们,例如JSON或XML文档,关系型数据库中的表、或者图数据模型。 -3. 开发数据库软件的工程师需要决定如何使用内存、磁盘或网络中的字节来表示这些数据结构(JSON/XML/关系/图)。这种表示可以允许数据以各种方式被查询,搜索,操纵和处理。 -4. 在更低的层次上,硬件工程师已经想出了使用电流,光脉冲,磁场或其他东西来表示这些字节的方法。 +1. 作为一名应用开发人员,你观察现实世界(里面有人员,组织,货物,行为,资金流向,传感器等),并采用对象或数据结构,以及操控那些数据结构的API来进行建模。那些结构通常是特定于应用程序的。 +2. 当要存储那些数据结构时,你可以利用通用数据模型来表示它们,如JSON或XML文档,关系数据库中的表、或图模型。 +3. 数据库软件的工程师选定如何以内存、磁盘或网络上的字节来表示JSON/XML/关系/图数据。这类表示形式使数据有可能以各种方式来查询,搜索,操纵和处理。 +4. 在更低的层次上,硬件工程师已经想出了使用电流,光脉冲,磁场或者其他东西来表示字节的方法。 -在一个复杂的应用中,可能会有更多的中间层,比如基于API 的API,但是基本思想仍然是一样的:每个层都通过提供一个干净整洁的数据模型来隐藏更低层次中的复杂度。这些抽象允许不同的人群有效地协作(例如数据库厂商的工程师,与使用其数据库的应用开发者)。 +一个复杂的应用程序可能会有更多的中间层次,比如基于API的API,不过基本思想仍然是一样的:每个层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性。这些抽象允许不同的人群有效地协作(例如数据库厂商的工程师和使用数据库的应用程序开发人员)。 -有许多不同类型的数据模型,每个数据模型都带有如何使用的假设。某些用法很容易,有些不被支持;一些操作很快,一些操作不好;一些数据转换感觉自然,有些是尴尬的。 +数据模型种类繁多,每个数据模型都带有如何使用的设想。有些用法很容易,有些则不支持如此;有些操作运行很快,有些则表现很差;有些数据转换非常自然,有些则很麻烦。 -掌握一个数据模型可能需要很多努力(想想关系数据建模有多少本书)。即使只使用一种数据模型,而不用担心其内部工作,构建软件也是非常困难的。但是由于数据模型对软件的功能有很大的影响,因此选择适合应用程序的软件是非常重要的。 +掌握一个数据模型需要花费很多精力(想想关系数据建模有多少本书)。即便只使用一个数据模型,不用操心其内部工作机制,构建软件也是非常困难的。然而,因为数据模型对上层软件的功能(能做什么,不能做什么)有着至深的影响,所以选择一个适合的数据模型是非常重要的。 -在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别是,我们将比较关系模型,文档模型和一些基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在第3章中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型是如何实际实现的(列表中的第3点)。 +在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在第3章中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第3点)。 ## 关系模型与文档模型 -现在最著名的数据模型可能是SQL,它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织到关系中(称为SQL表),其中每个关系是元组的无序集合SQL中的行)。 +现在最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织成**关系**(SQL中称作**表**),其中每个关系是**元组**(SQL中称作**行**)的无序集合。 -关系模型是一个理论上的提议,当时很多人都怀疑是否能够有效实现。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数需要存储和查询具有某种规模结构的数据的人们的首选工具。关系数据库的优势已经持续了大约25~30年——计算史中的永恒。 +关系模型曾是一个理论性的提议,当时很多人都怀疑是否能够有效实现它。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数人们存储和查询某些常规结构的数据的首选工具。关系数据库已经持续称霸了大约25~30年——这对计算机史来说是极其漫长的时间。 -关系数据库起源于商业数据处理,这是在20世纪60年代和70年代在大型计算机上进行的。从今天的角度来看,用例显得很平常:通常是交易处理(进入销售或银行交易,航空公司预订,仓库库存)和批处理(客户发票,工资单,报告)。 +关系数据库起源于商业数据处理,在20世纪60年代和70年代用大型计算机来执行。从今天的角度来看,那些用例显得很平常:典型的**事务处理**(将销售或银行交易,航空公司预订,库存管理信息记录在库)和**批处理**(客户发票,工资单,报告)。 -当时的其他数据库迫使应用程序开发人员考虑数据库内部的数据表示。关系模型的目标是将实现细节隐藏在更简洁的界面之后。 +当时的其他数据库迫使应用程序开发人员必须考虑数据库内部的数据表示形式。关系模型致力于将上述实现细节隐藏在更简洁的接口之后。 -多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型是主要的选择,但关系模型占据了主导地位。对象数据库在二十世纪八十年代末和九十年代初再次出现。 XML数据库出现在二十一世纪初,但只有小众采用。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。 +多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型曾是主要的选择,但关系模型随后占据了主导地位。对象数据库在20世纪80年代末和90年代初来了又去。 XML数据库在二十一世纪初出现,但只有小众采用过。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。 -随着电脑越来越强大和联网,它们开始被用于日益多样化的目的。值得注意的是,关系数据库在业务数据处理的原始范围之外被推广到很广泛的用例。您今天在网上看到的大部分内容仍然是由关系数据库提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等。 +随着电脑越来越强大和互联,它们开始用于日益多样化的目的。关系数据库非常成功地被推广到业务数据处理的原始范围之外更为广泛的用例上。您今天在网上看到的大部分内容依旧是由关系数据库来提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等内容。 ### NoSQL的诞生 -现在在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字非常不幸,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的一个关于分布式,非关系数据库上的开源聚会。无论如何,这个术语触动了某些神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与*#NoSQL#*标签相关联,并被追溯性地重新解释为不仅是SQL 【4】。 +现在 - 2010年代,NoSQL开始了最新一轮尝试,试图推翻关系模型的统治地位。 “NoSQL”这个名字让人遗憾,因为实际上它并没有涉及到任何特定的技术。最初它只是作为一个醒目的Twitter标签,用在2009年一个关于分布式,非关系数据库上的开源聚会上。无论如何,这个术语触动了某些神经,并迅速在网络创业社区内外传播开来。好些有趣的数据库系统现在都与*#NoSQL#*标签相关联,并且NoSQL被追溯性地重新解释为**不仅是SQL(Not Only SQL)** 【4】。 -采用NoSQL数据库有几个驱动力,其中包括: +采用NoSQL数据库的背后有几个驱动因素,其中包括: * 需要比关系数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量 -* 相比商业数据库产品,偏爱免费和开源软件 +* 相比商业数据库产品,免费和开源软件更受偏爱。 * 关系模型不能很好地支持一些特殊的查询操作 -* 对关系模型限制性感到受挫,对更多动态性与表现力的渴望 +* 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型【5】 -不同的应用程序有不同的要求,一个用例的最佳技术选择可能不同于另一个用例的最佳选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为**混合持久化(Polyglot Persistences)** +不同的应用程序有不同的需求,一个用例的最佳技术选择可能不同于另一个用例的最佳技术选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为**混合持久化(polyglot persistence)** ### 对象关系不匹配 -现在大多数应用程序开发都是在面向对象的编程语言中完成的,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么应用程序代码中的对象之间需要一个笨拙的转换层,表,行和列的数据库模型。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。 +目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。 -[^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题 +[^i]: 一个从电子学借用的术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配会导致信号反射及其他问题。 -像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。 +像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。 ![](img/fig2-1.png) -**图2-1 使用关系型模式来表示领英简历** +**图2-1 使用关系型模式来表示领英简介** -例如,[图2-1](img/fig2-1.png)展示了如何在关系模式中表达简历(一个LinkedIn简介)。整个配置文件可以通过一个唯一的标识符`user_id`来标识。像`first_name`和`last_name`这样的字段每个用户只出现一次,所以他们可以在用户表上建模为列。但是,大多数人的职业(职位)多于一份工作,人们可能有不同的教育期限和不同数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示: +例如,[图2-1](img/fig2-1.png)展示了如何在关系模式中表示简历(一个LinkedIn简介)。整个简介可以通过一个唯一的标识符`user_id`来标识。像`first_name`和`last_name`这样的字段每个用户只出现一次,所以他们可以在用户表上建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示: -* 在传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,培训和联系信息放在单独的表中,对用户表提供外键引用,如[图2-1](img/fig2-1.png)所示。 -* 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也受到几个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。 -* 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解释其结构和内容。在这种配置中,通常不能使用数据库查询该编码列中的值。 +* 传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对用户表提供外键引用,如[图2-1](img/fig2-1.png)所示。 +* 后续的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,并支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也得到多个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。 +* 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解析其结构和内容。这种配置下,通常不能使用数据库来查询该编码列中的值。 -对于一个像简历这样自包含的数据结构而言,JSON表示是非常合适的:参见[例2-1]()。 JSON比XML更简单。 面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。 +对于一个像简历这样自包含文档的数据结构而言,JSON表示是非常合适的:参见[例2-1]()。JSON比XML更简单。面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。 +**例2-1. 用JSON文档表示一个LinkedIn简介** ```json { @@ -115,11 +116,11 @@ } ``` -一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。但是,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。 +有一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。 -JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的局部性。如果要在关系示例中获取配置文件,则需要执行多个查询(通过`user_id`查询每个表),或者在用户表与其下属表之间执行混乱的多路连接。在JSON表示中,所有的相关信息都在一个地方,一个查询就足够了。 +JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部性(locality)**。如果在上面的关系型示例中获取简介,那需要执行多个查询(通过`user_id`查询每个表),或者在用户表与其下属表之间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。 -从用户配置文件到用户位置,教育历史和联系信息的一对多关系意味着数据中的树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。 +从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。 ![](img/fig2-2.png) @@ -127,148 +128,148 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的局部性 ### 多对一和多对多的关系 -在上一节的[例2-1]()中,`region_id`和`industry_id`是以ID而不是纯字符串“大西雅图地区”和“慈善”的形式给出的。为什么? +在上一节的[例2-1]()中,`region_id`和`industry_id`是以ID,而不是纯字符串“Greater Seattle Area”和“Philanthropy”的形式给出的。为什么? -如果用户界面具有用于输入区域和行业的自由文本字段,则将其存储为纯文本字符串是有意义的。但是,对地理区域和行业进行标准化,并让用户从下拉列表或自动填充器中进行选择是有好处的: +如果用户界面用一个自由文本字段来输入区域和行业,那么将他们存储为纯文本字符串是合理的。另一方式是给出地理区域和行业的标准化的列表,并让用户从下拉列表或自动填充器中进行选择,其优势如下: -* 统一的样式和拼写 +* 各个简介之间样式和拼写统一 * 避免歧义(例如,如果有几个同名的城市) -* 易于更新——名称只存储在一个地方,所以如果需要更改(例如,由于政治事件而改变城市名称),便于全面更新。 -* 本地化支持——当网站翻译成其他语言时,标准化的名单可以被本地化,所以地区和行业可以使用用户的语言来表示 -* 更好的搜索——例如,搜索华盛顿州的慈善家可以匹配这份简历,因为地区列表可以编码记录西雅图在华盛顿的事实(从“大西雅图地区”这个字符串中看不出来) +* 易于更新——名称只存储在一个地方,如果需要更改(例如,由于政治事件而改变城市名称),很容易进行全面更新。 +* 本地化支持——当网站翻译成其他语言时,标准化的列表可以被本地化,使得地区和行业可以使用用户的语言来显示 +* 更好的搜索——例如,搜索华盛顿州的慈善家就会匹配这份简介,因为地区列表可以编码记录西雅图在华盛顿这一事实(从“Greater Seattle Area”这个字符串中看不出来) -无论是存储一个ID还是一个文本字符串,都是一个关于**重复**的问题。当你使用一个ID时,对人类有意义的信息(比如单词:慈善)只存储在一个地方,引用它的所有信息都使用一个ID(ID只在数据库中有意义)。当你直接存储文本时,每个使用它的记录中,都存储的是有意义的信息。 +存储ID还是文本字符串,这是个**复制(duplication)**问题。当使用ID时,对人类有意义的信息(比如单词:Philanthropy)只存储在一处,所有引用它的地方使用ID(ID只在数据库中有意义)。当直接存储文本时,对人类有意义的信息会复制在每处使用记录中。 -使用ID的好处是,因为它对人类没有任何意义,所以永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,并且存在不一致的风险(信息的一些副本被更新,但其他信息的副本不被更新)。去除这种重复是数据库规范化的关键思想。(关系模型区分了几种不同的范式,但这些区别实际上并不重要。 作为一个经验法则,如果您重复只能存储在一个地方的值,那么架构不会被**规范化(normalized)**[^ii]。) +使用ID的好处是,ID对人类没有任何意义,永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库**规范化(normalization)**的关键思想。[^ii] -[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。 作为一个经验法则,如果重复存储了只能存储在一个地方的值,则模式就不是规范化的。 +[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化(normalized)**的。 -> 数据库管理员和开发人员喜欢争论规范化和非规范化,但我们现在暂停判断。 在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨处理缓存,非规范化和派生数据的系统方法。 +> 数据库管理员和开发人员喜欢争论规范化和非规范化,让我们暂时保留判断吧。在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨系统的方法用以处理缓存,非规范化和派生数据。 -不幸的是,对这些数据进行规范化需要多对一的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作),这与文档模型不太吻合。在关系数据库中,通过ID来引用其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构不需要连接,对连接的支持通常很弱[^iii]。 +不幸的是,对这些数据进行规范化需要多对一的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作),这与文档模型不太吻合。在关系数据库中,通过ID来引用其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构没有必要用连接,对连接的支持通常很弱[^iii]。 -[^iii]: 在撰写本文时,RethinkDB支持连接,MongoDB不支持连接,并且只支持CouchDB中的预先声明的视图。 +[^iii]: 在撰写本文时,RethinkDB支持连接,MongoDB不支持连接,而CouchDB只支持预先声明的视图。 -如果数据库本身不支持连接,则必须通过对数据库进行多个查询来模拟应用程序代码中的连接。 (在这种情况下,地区和行业的名单可能很小,变化不大,应用程序可以简单地将它们留在内存中,但是,联接的工作从数据库转移到应用程序代码。 +如果数据库本身不支持连接,则必须在应用程序代码中通过对数据库进行多个查询来模拟连接。(在这种情况中,地区和行业的列表可能很小,改动很少,应用程序可以简单地将其保存在内存中。不过,执行连接的工作从数据库被转移到应用程序代码上。 -而且,即使应用程序的初始版本适合无连接的文档模型,随着功能添加到应用程序中,数据也会变得更加互联。例如,考虑一下我们可以对简历例子进行的一些修改: +此外,即便应用程序的最初版本适合无连接的文档模型,随着功能添加到应用程序中,数据会变得更加互联。例如,考虑一下对简历例子进行的一些修改: ***组织和学校作为实体*** -在前面的描述中,组织(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的标识和其他信息(参见[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。 +在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(参见[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。 ***推荐*** -假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。推荐在用户的简历上显示,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,他们写的任何建议都需要反映新的照片。因此,推荐应该引用作者的个人资料。 - +假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,他们写的任何建议都需要显示新的照片。因此,推荐应该有作者个人简介的引用。 ![](img/fig2-3.png) -**图2-3 公司名不仅是字符串,还是一个指向公司实体的连接(领英截图)** +**图2-3 公司名不仅是字符串,还是一个指向公司实体的链接(LinkedIn截图)** -[图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。 +[图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示成引用,并且在查询时需要连接。 ![](img/fig2-4.png) **图2-4 使用多对多关系扩展简历** -### 文档数据库是否在重蹈覆辙? +### 文档数据库是否在重演历史? + +在多对多的关系和连接已常规用在关系数据库时,文档数据库和NoSQL重启了辩论:如何最好地在数据库中表示多对多关系。这个辩论可比NoSQL古老得多,事实上,最早可以追溯到计算机化数据库系统。 -虽然关系数据库中经常使用多对多的关系和连接,但文档数据库和NoSQL重新讨论了如何最好地在数据库中表示这种关系的争论。这个辩论比NoSQL早得多,事实上,它可以追溯到最早的计算机化数据库系统。 +20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了阿波罗太空计划的库存管理而开发的,并于1968年有了首次商业发布【13】。目前它仍在使用和维护,运行在IBM大型机的OS/390上【14】。 -20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了在阿波罗太空计划中进行库存管理而开发的,并于1968年首次商业发布【13】。目前它仍在使用和维护,在IBM大型机的OS/390上运行【14】。 -IMS的设计使用了一个相当简单的数据模型,称为层次模型,它与文档数据库使用的JSON模型有一些显着的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树,就像[图2-2](img/fig2-2.png)的JSON结构一样。 +IMS的设计中使用了一个相当简单的数据模型,称为**层次模型(hierarchical model)**,它与文档数据库使用的JSON模型有一些惊人的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树,这很像[图2-2](img/fig2-2.png)的JSON结构。 -像文档数据库一样,IMS在一对多的关系中运行良好,但是它使多对多的关系变得困难,并且不支持连接。开发人员必须决定是否冗余(非规范化)数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与开发人员今天遇到的文档数据库问题非常相似【15】。 +同文档数据库一样,IMS能良好处理一对多的关系,但是很难应对多对多的关系,并且不支持连接。开发人员必须决定是否复制(非规范化)数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与现在开发人员遇到的文档数据库问题非常相似【15】。 -提出了各种解决方案来解决层次模型的局限性。其中最突出的两个是关系模型(它变成了SQL,接管了世界)和网络模型(最初很受关注,但最终变得模糊)。这两个阵营之间的“大辩论”持续了70年代的大部分时间【2】。 +那时人们提出了各种不同的解决方案来解决层次模型的局限性。其中最突出的两个是**关系模型(relational model)**(它变成了SQL,统治了世界)和**网络模型(network model)**(最初很受关注,但最终变得冷门)。这两个阵营之间的“大辩论”在70年代持续了很久时间【2】。 -由于这两个模式解决的问题今天仍然如此相关,今天的辩论值得简要回顾一下。 +那两个模式解决的问题与当前的问题相关,因此值得简要回顾一下那场辩论。 #### 网络模型 -网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并由几个不同的数据源进行实施;它也被称为CODASYL模型【16】。 +网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并被数个不同的数据库商实现;它也被称为CODASYL模型【16】。 -CODASYL模型是层次模型的推广。在分层模型的树结构中,每条记录只有一个父节点,在网络模式中,一个记录可能有多个父母。例如,“大西雅图地区”地区可能有一条记录,而且每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。 +CODASYL模型是层次模型的推广。在层次模型的树结构中,每条记录只有一个父节点;在网络模式中,每条记录可能有多个父节点。例如,“Greater Seattle Area”地区可能是一条记录,每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。 -网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是沿着这些链路链上的根记录进行路径。这被称为**访问路径**。 +网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是跟随从根记录起沿这些链路所形成的路径。这被称为**访问路径(access path)**。 -在最简单的情况下,访问路径可能类似于遍历链表:从列表头开始,一次查看一条记录,直到找到所需的记录。但在一个多对多关系的世界里,几条不同的路径可能会导致相同的记录,一个使用网络模型的程序员必须跟踪这些不同的访问路径。 +最简单的情况下,访问路径类似遍历链表:从列表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,数条不同的路径可以到达相同的记录,网络模型的程序员必须跟踪这些不同的访问路径。 -CODASYL中的查询是通过遍历记录列表和访问路径后,通过在数据库中移动游标来执行的。如果记录有多个父母(即来自其他记录的多个传入指针),则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认,这就像在一个n维数据空间中进行导航【17】。 +CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数据库中移动游标来执行的。如果记录有多个父结点(即多个来自其他记录的传入指针),则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认,这就像在n维数据空间中进行导航【17】。 -尽管手动访问路径选择能够最有效地利用20世纪70年代非常有限的硬件功能(如磁带驱动器,其搜索速度非常慢),但问题是他们使查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型,如果你没有一个你想要的数据的路径,那么你就处于一个困难的境地。你可以改变访问路径,但是你必须经过大量的手写数据库查询代码,并重写它来处理新的访问路径。很难对应用程序的数据模型进行更改。 +尽管手动选择访问路径够能最有效地利用20世纪70年代非常有限的硬件功能(如磁带驱动器,其搜索速度非常慢),但这使得查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型,如果你没有所需数据的路径,就会陷入困境。你可以改变访问路径,但是必须浏览大量手写数据库查询代码,并重写来处理新的访问路径。更改应用程序的数据模型是很难的。 #### 关系模型 -相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(table)**只是一个**元组(行)**的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。 +相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(表)**只是一个**元组(行)**的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。 -[^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束,在查询时执行外键连接,而在CODASYL中,连接在插入时高效完成。 +[^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。即使有约束,查询时会执行外键连接,而在CODASYL中,连接在插入时高效完成。 在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。 -如果你想以新的方式查询你的数据,你可以声明一个新的索引,查询会自动使用哪个索引是最合适的。您不需要更改查询来利用新的索引。 (请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使向应用程序添加新功能变得更加容易。 +如果想按新的方式查询数据,可以声明一个新的索引,查询会自动使用最合适的那些索引。无需更改查询来利用新的索引。 (请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使添加应用程序新功能变得更加容易。 -关系数据库的查询优化器是复杂的,他们已经耗费了多年的研究和开发工作【18】。但关系模型的一个关键洞察是:只需构建一次查询优化器,然后使用该数据库的所有应用程序都可以从中受益。如果您没有查询优化器,那么为特定查询手动编写访问路径比编写通用优化器更容易——但通用解决方案从长期看更好。 +关系数据库的查询优化器是复杂的,耗费多年的研究和开发精力【18】。关系模型的一个关键洞察是:只需构建一次查询优化器,随后使用该数据库的所有应用程序都可以从中受益。如果你没有查询优化器的话,那么为特定查询手动编写访问路径比编写通用优化器更容易——不过通用解决方案从长期看更好。 #### 与文档数据库相比 -文档数据库在一个方面还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如位置,教育和`contact_info`),而不是在单独的表中。 +在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如`positions`,`education`和`contact_info`),而不是在单独的表中。 -但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为外键,在文档模型中称为文档引用【9】。该标识符在读取时通过使用加入或后续查询来解决。迄今为止,文档数据库没有遵循CODASYL的路径。 +但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为**外键**,在文档模型中称为**文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止,文档数据库没有遵循CODASYL的路数。 ### 关系型数据库与文档数据库在今日的对比 -将关系数据库与文档数据库进行比较时,需要考虑许多差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。在本章中,我们将只关注数据模型中的差异。 +将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。本章将只关注数据模型中的差异。 -支持文档数据模型的主要论据是架构灵活性,由于局部性而导致的更好的性能,对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。 +支持文档数据模型的主要论据是架构灵活性,因局部性而拥有更好的性能,以及对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。 #### 哪个数据模型更方便写代码? -如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常整个树被一次加载),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。 +如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。 -文档模型有一定的局限性:例如,你不能直接引用文档中的需要的项目,而是需要说“用户251的位置列表中的第二项”(很像访问路径在分层模型中)。但是,只要文件嵌套不太深,通常不是问题。 +文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说“用户251的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,通常不是问题。 -应用程序对文档数据库连接的垃圾支持也许或也许不是一个问题。例如,在使用文档数据库记录 哪个事件发生在哪儿 的分析应用程序中,可能永远不需要多对多的关系【19】。 +对文档数据库连接的糟糕支持也许或也许不是一个问题,这取决于应用程序。例如,分析应用程可能永远不需要多对多的关系,如果它使用文档数据库来记录何事发生于何时【19】。 但是,如果您的应用程序确实使用多对多关系,那么文档模型就没有那么吸引人了。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能【15】。 -说哪个数据模型在一般情况下导致更简单的应用程序代码是不可能的;它取决于数据项之间存在的关系种类。对于高度相互关联的数据,文档模型很尴尬,关系模型是可接受的,而图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。 +很难说在一般情况下哪个数据模型让应用程序代码更简单;它取决于数据项之间存在的关系种类。对于高度相联的数据,文档模型是糟糕的,关系模型是可接受的,而图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。 #### 文档模型中的架构灵活性 -大多数文档数据库以及关系数据库中的JSON支持都不会对文档中的数据执行任何模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中,并且在读取时,客户端对于文档可能包含的字段没有保证。 +大多数文档数据库以及关系数据库中的JSON支持都不会强制文档中的数据采用何种模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中,并且当读取时,客户端对无法保证文档可能包含的字段。 -文档数据库有时称为**无模式(schemaless)**,但这是误导性的,因为读取数据的代码通常采用某种结构——即存在隐式模式,但不由数据库强制执行【20】。一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)【21】。 +文档数据库有时称为**无模式(schemaless)**,但这是误导性的,因为读取数据的代码通常假定某种结构——即存在隐式模式,但不由数据库强制执行【20】。一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法,模式明确,且数据库确保所有的数据都符合其模式)【21】。 -读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议【22】,数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。 +读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的相对优点具有很大的争议性【22】,数据库中模式的强制性是一个具有争议的话题,一般来说没有正确或错误的答案。 -在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏【23】。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如: +在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你把每个用户的全名存储在一个字段中,而现在想分别存储名字和姓氏【23】。在文档数据库中,只需开始写入具有新字段的新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如: ```go if (user && user.name && !user.first_name) { - // Documents written before Dec 8, 2013 don't have first_name + // Documents written before Dec 8, 2013 don't have first_name user.first_name = user.name.split(" ")[0]; } ``` -另一方面,在“静态类型”数据库模式中,通常会执行以下操作: +另一方面,在“静态类型”数据库模式中,通常会执行以下**迁移(migration)**操作: ```sql ALTER TABLE users ADD COLUMN first_name text; -UPDATE users SET first_name = split_part(name, ' ', 1); -- PostgreSQL +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`时会复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制【24,25,26】。 +模式变更的速度很慢,而且需要停运。这种坏声誉并不是完全应得的:大多数关系数据库系统可在几毫秒内执行`ALTER TABLE`语句。 MySQL是一个值得注意的例外,它执行`ALTER TABLE`时会复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制【24,25,26】。 -在大型表上运行`UPDATE`语句在任何数据库上都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将`first_name`设置为默认值`NULL`,并在读取时填充它,就像使用文档数据库一样。 +在大型表上运行`UPDATE`语句在任何数据库上都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将`first_name`设置为默认值`NULL`,并在读取时再填充,就像使用文档数据库一样。 -如果由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为: +读时模式更具优势,当由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为: * 有许多不同类型的对象,将每种类型的对象放在自己的表中是不现实的。 -* 数据的结构由您无法控制,且随时可能更改的外部系统决定。 +* 数据的结构由您无法控制且随时可能变化的外部系统决定。 -在这样的情况下,模式的伤害远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,如果所有记录都有相同的结构,那么模式就是记录和强制这种结构的有用机制。我们将在第四章更详细地讨论模式和模式演化。 +在这样的情况下,模式的伤害远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,如果所有记录都具有相同的结构,那么模式就是记录并强制这种结构的有用机制。第四章将更详细地讨论模式和模式演化。 #### 查询的数据局部性 @@ -367,7 +368,7 @@ SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变 现在想让当前所选页面的标题有一个蓝色的背景,以便在视觉上突出显示。 使用CSS实现起来非常简单: ```css -li.selected > p { +li.selected > p { background-color: blue; } ``` @@ -552,7 +553,7 @@ db.observations.aggregate([ * 唯一标识符 * **边的起点/尾点(tail vertex)** -* **边的终点/头点(head vertex)** +* **边的终点/头点(head vertex)** * 描述两个顶点之间关系类型的标签 * 一组属性(键值对) @@ -613,7 +614,7 @@ CREATE ```cypher MATCH (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}), - (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'}) + (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'}) RETURN person.name ``` @@ -649,24 +650,24 @@ WITH RECURSIVE in_usa(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'United States' UNION - SELECT edges.tail_vertex FROM edges + SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'within' ), -- in_europe 包含所有的欧洲境内的地点ID in_europe(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'Europe' - UNION + UNION SELECT edges.tail_vertex FROM edges JOIN in_europe ON edges.head_vertex = in_europe.vertex_id WHERE edges.label = 'within' ), - + -- born_in_usa 包含了所有类型为Person,且出生在美国的顶点 born_in_usa(vertex_id) AS ( SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'born_in' ), - + -- lives_in_europe 包含了所有类型为Person,且居住在欧洲的顶点。 lives_in_europe(vertex_id) AS ( SELECT edges.tail_vertex FROM edges @@ -816,7 +817,7 @@ SPARQL是一种很好的查询语言——即使语义网从来没有出现, > #### 图形数据库与网络模型相比较 > -> 在“[文档数据库是否在重蹈覆辙?](#文档数据库是否在重蹈覆辙?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种? +> 在“[文档数据库是否在重演历史?](#文档数据库是否在重演历史?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种? > > 不,他们在几个重要方面有所不同: > @@ -862,15 +863,15 @@ born_in(lucy, idaho). ``` within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */ -within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */ +within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */ within_recursive(Via, Name). -migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */ +migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */ born_in(Person, BornLoc), within_recursive(BornLoc, BornIn), lives_in(Person, LivingLoc), within_recursive(LivingLoc, LivingIn). - + ?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */ ``` @@ -1035,4 +1036,3 @@ Datalog方法需要对本章讨论的其他查询语言采取不同的思维方 | 上一章 | 目录 | 下一章 | | -------------------------------------- | ------------------------------- | ---------------------------- | | [第一章:可靠、可扩展、可维护](ch1.md) | [设计数据密集型应用](README.md) | [第三章:存储与检索](ch3.md) | -