mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
Merge remote-tracking branch 'refs/remotes/Vonng/master'
This commit is contained in:
commit
7fadf50e0f
53
README.md
53
README.md
@ -54,16 +54,16 @@
|
||||
|
||||
## 目录
|
||||
|
||||
### [序](preface.md)
|
||||
### [序言](preface.md)
|
||||
|
||||
### [数据系统的基石](part-i.md)
|
||||
### [第一部分:数据系统的基石](part-i.md)
|
||||
|
||||
* [第一章:可靠性、可扩展性、可维护性](ch1.md)
|
||||
* [第二章:数据模型与查询语言](ch2.md)
|
||||
* [第三章:存储与检索](ch3.md)
|
||||
* [第四章:编码与演化](ch4.md)
|
||||
|
||||
### [分布式数据](part-ii.md)
|
||||
### [第二部分:分布式数据](part-ii.md)
|
||||
|
||||
* [第五章:复制](ch5.md)
|
||||
* [第六章:分区](ch6.md)
|
||||
@ -71,7 +71,7 @@
|
||||
* [第八章:分布式系统的麻烦](ch8.md)
|
||||
* [第九章:一致性与共识](ch9.md)
|
||||
|
||||
### [派生数据](part-iii.md)
|
||||
### [第三部分:衍生数据](part-iii.md)
|
||||
|
||||
* [第十章:批处理](ch10.md)
|
||||
* [第十一章:流处理](ch11.md)
|
||||
@ -83,8 +83,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 翻译计划
|
||||
|
||||
* 机翻:只在乎结构:梳理文章结构、图片、引用、备注。
|
||||
@ -93,26 +91,26 @@
|
||||
|
||||
精翻可以看,机翻基本没法看,初翻对于业内人士能凑合看。
|
||||
|
||||
| 章节 | 进度 | 锁定 |
|
||||
| :--------------------------------: | :------: | :---: |
|
||||
| 序言 | 初翻 | |
|
||||
| 第一部分:数据系统基础 ——概览 | 精翻 | |
|
||||
| 第一章:可靠性、可扩展性、可维护性 | 精翻 | |
|
||||
| 第二章:数据模型与查询语言 | 初翻 | |
|
||||
| 第三章:存储与检索 | 初翻 | |
|
||||
| 第四章:编码与演化 | 初翻 | |
|
||||
| 第二部分:分布式数据——概览 | 精翻 | |
|
||||
| 第五章:复制 | 精翻 30% | Vonng |
|
||||
| 第六章:分区 | 初翻 | |
|
||||
| 第七章:事务 | 精翻 60% | Vonng |
|
||||
| 第八章:分布式系统中的问题 | 初翻 | |
|
||||
| 第九章:一致性与共识 | 初翻 | Vonng |
|
||||
| 第三部分:前言 | 精翻 | |
|
||||
| 第十章:批处理 | 草翻 | |
|
||||
| 第十一章:流处理 | 草翻 | |
|
||||
| 第十二章:数据系统的未来 | 草翻 | |
|
||||
| 术语表 | - | |
|
||||
| 后记 | 机翻 | |
|
||||
| 章节 | 进度 | 锁定 |
|
||||
| :--------------------------------: | :------: | :-----------: |
|
||||
| 序言 | 初翻 | |
|
||||
| 第一部分:数据系统基础 | 精翻 | |
|
||||
| 第一章:可靠性、可扩展性、可维护性 | 精翻 | |
|
||||
| 第二章:数据模型与查询语言 | 初翻 | @ jiajiadebug |
|
||||
| 第三章:存储与检索 | 初翻 40% | Vonng |
|
||||
| 第四章:编码与演化 | 初翻 | |
|
||||
| 第二部分:分布式数据 | 精翻 | |
|
||||
| 第五章:复制 | 精翻 30% | Vonng |
|
||||
| 第六章:分区 | 初翻 | |
|
||||
| 第七章:事务 | 精翻 60% | Vonng |
|
||||
| 第八章:分布式系统中的问题 | 初翻 | |
|
||||
| 第九章:一致性与共识 | 初翻 | |
|
||||
| 第三部分:衍生数据 | 精翻 | |
|
||||
| 第十章:批处理 | 草翻 | |
|
||||
| 第十一章:流处理 | 草翻 | |
|
||||
| 第十二章:数据系统的未来 | 草翻 | |
|
||||
| 术语表 | - | |
|
||||
| 后记 | 机翻 | |
|
||||
|
||||
|
||||
|
||||
@ -127,8 +125,7 @@ All contribution will give proper credit. 贡献者需要同意[法律声明](#
|
||||
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
|
||||
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
|
||||
|
||||
4. 第一部分前言,ch2校正 by @jiajiadebug
|
||||
|
||||
|
||||
|
||||
|
2
ch1.md
2
ch1.md
@ -384,7 +384,7 @@
|
||||
|
||||
**可维护性(Maintainability)**有许多方面,但实质上是关于工程师和运维团队的生活质量的。良好的抽象可以帮助降低复杂度,并使系统易于修改和适应新的应用场景。良好的可操作性意味着对系统的健康状态具有良好的可见性,并拥有有效的管理手段。
|
||||
|
||||
不幸的是,使应用可靠、可扩展或可持续并不容易。但是某些模式和技术会不断重新出现在不同的应用中。在接下来的几章中,我们将看到一些数据系统的例子,并分析它们如何实现这些目标。
|
||||
不幸的是,使应用可靠、可扩展或可维护并不容易。但是某些模式和技术会不断重新出现在不同的应用中。在接下来的几章中,我们将看到一些数据系统的例子,并分析它们如何实现这些目标。
|
||||
|
||||
在本书后面的[第三部分](part-iii.md)中,我们将看到一种模式:几个组件协同工作以构成一个完整的系统(如[图1-1](img/fig1-1.png)中的例子)
|
||||
|
||||
|
2
ch12.md
2
ch12.md
@ -18,7 +18,7 @@
|
||||
|
||||
## 数据集成
|
||||
|
||||
本书中反复出现的主题是,对于任何给定的问题,都有几种解决方案,所有这些解决方案都有不同的优点,缺点和折衷。例如,在[第3章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树和列式存储。在[第5章](ch5.md)讨论复制时,我们看到了单领导,多领导和无领导的方法。
|
||||
本书中反复出现的主题是,对于任何给定的问题,都有几种解决方案,所有这些解决方案都有不同的优点,缺点和折衷。例如,在[第3章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树和列存储。在[第5章](ch5.md)讨论复制时,我们看到了单领导,多领导和无领导的方法。
|
||||
|
||||
如果你有一个问题,例如“我想存储一些数据并稍后再查询”,那么没有一个正确的解决方案,但是在不同的情况下,每种方法都是适当的。软件实现通常必须选择一种特定的方法。要使一个代码路径健壮并且很好地尝试在一个软件中执行所有操作几乎可以保证实现效果很差,这很难。
|
||||
|
||||
|
379
ch2.md
379
ch2.md
@ -40,13 +40,13 @@
|
||||
|
||||
当时的其他数据库迫使应用程序开发人员必须考虑数据库内部的数据表示形式。关系模型致力于将上述实现细节隐藏在更简洁的接口之后。
|
||||
|
||||
多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型曾是主要的选择,但关系模型随后占据了主导地位。对象数据库在20世纪80年代末和90年代初来了又去。 XML数据库在二十一世纪初出现,但只有小众采用过。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。
|
||||
多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型曾是主要的选择,但关系模型随后占据了主导地位。对象数据库在20世纪80年代末和90年代初来了又去。XML数据库在二十一世纪初出现,但只有小众采用过。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。
|
||||
|
||||
随着电脑越来越强大和互联,它们开始用于日益多样化的目的。关系数据库非常成功地被推广到业务数据处理的原始范围之外更为广泛的用例上。您今天在网上看到的大部分内容依旧是由关系数据库来提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等内容。
|
||||
随着电脑越来越强大和互联,它们开始用于日益多样化的目的。关系数据库非常成功地被推广到业务数据处理的原始范围之外更为广泛的用例上。你今天在网上看到的大部分内容依旧是由关系数据库来提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等内容。
|
||||
|
||||
### NoSQL的诞生
|
||||
|
||||
现在 - 2010年代,NoSQL开始了最新一轮尝试,试图推翻关系模型的统治地位。 “NoSQL”这个名字让人遗憾,因为实际上它并没有涉及到任何特定的技术。最初它只是作为一个醒目的Twitter标签,用在2009年一个关于分布式,非关系数据库上的开源聚会上。无论如何,这个术语触动了某些神经,并迅速在网络创业社区内外传播开来。好些有趣的数据库系统现在都与*#NoSQL#*标签相关联,并且NoSQL被追溯性地重新解释为**不仅是SQL(Not Only SQL)** 【4】。
|
||||
现在 - 2010年代,NoSQL开始了最新一轮尝试,试图推翻关系模型的统治地位。“NoSQL”这个名字让人遗憾,因为实际上它并没有涉及到任何特定的技术。最初它只是作为一个醒目的Twitter标签,用在2009年一个关于分布式,非关系数据库上的开源聚会上。无论如何,这个术语触动了某些神经,并迅速在网络创业社区内外传播开来。好些有趣的数据库系统现在都与*#NoSQL#*标签相关联,并且NoSQL被追溯性地重新解释为**不仅是SQL(Not Only SQL)** 【4】。
|
||||
|
||||
采用NoSQL数据库的背后有几个驱动因素,其中包括:
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。
|
||||
|
||||
[^i]: 一个从电子学借用的术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配会导致信号反射及其他问题。
|
||||
[^i]: 一个从电子学借用的术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当你将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配会导致信号反射及其他问题。
|
||||
|
||||
像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
|
||||
|
||||
@ -69,10 +69,10 @@
|
||||
|
||||
**图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`这样的字段每个用户只出现一次,所以可以在User表上将其建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示:
|
||||
|
||||
* 传统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】。
|
||||
* 传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对User表提供外键引用,如[图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】)支持这种数据模型。
|
||||
@ -118,9 +118,9 @@
|
||||
|
||||
有一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。
|
||||
|
||||
JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部性(locality)**。如果在上面的关系型示例中获取简介,那需要执行多个查询(通过`user_id`查询每个表),或者在用户表与其下属表之间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。
|
||||
JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部性(locality)**。如果在前面的关系型示例中获取简介,那需要执行多个查询(通过`user_id`查询每个表),或者在User表与其下属表之间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。
|
||||
|
||||
从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。
|
||||
从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的一个树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。
|
||||
|
||||
![](img/fig2-2.png)
|
||||
|
||||
@ -138,9 +138,9 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
|
||||
* 本地化支持——当网站翻译成其他语言时,标准化的列表可以被本地化,使得地区和行业可以使用用户的语言来显示
|
||||
* 更好的搜索——例如,搜索华盛顿州的慈善家就会匹配这份简介,因为地区列表可以编码记录西雅图在华盛顿这一事实(从“Greater Seattle Area”这个字符串中看不出来)
|
||||
|
||||
存储ID还是文本字符串,这是个**复制(duplication)**问题。当使用ID时,对人类有意义的信息(比如单词:Philanthropy)只存储在一处,所有引用它的地方使用ID(ID只在数据库中有意义)。当直接存储文本时,对人类有意义的信息会复制在每处使用记录中。
|
||||
存储ID还是文本字符串,这是个**副本(duplication)**问题。当使用ID时,对人类有意义的信息(比如单词:Philanthropy)只存储在一处,所有引用它的地方使用ID(ID只在数据库中有意义)。当直接存储文本时,对人类有意义的信息会复制在每处使用记录中。
|
||||
|
||||
使用ID的好处是,ID对人类没有任何意义,永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库**规范化(normalization)**的关键思想。[^ii]
|
||||
使用ID的好处是,ID对人类没有任何意义,因而永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库**规范化(normalization)**的关键思想。[^ii]
|
||||
|
||||
[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化(normalized)**的。
|
||||
|
||||
@ -160,20 +160,20 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
|
||||
|
||||
***推荐***
|
||||
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,他们写的任何建议都需要显示新的照片。因此,推荐应该有作者个人简介的引用。
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何建议都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
|
||||
![](img/fig2-3.png)
|
||||
|
||||
**图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】。
|
||||
|
||||
@ -203,19 +203,19 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
|
||||
|
||||
相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(表)**只是一个**元组(行)**的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。
|
||||
|
||||
[^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。即使有约束,查询时会执行外键连接,而在CODASYL中,连接在插入时高效完成。
|
||||
[^iv]: 外键约束允许对修改约束,但对于关系模型这并不是必选项。即使有约束,外键连接在查询时执行,而在CODASYL中,连接在插入时高效完成。
|
||||
|
||||
在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。
|
||||
|
||||
如果想按新的方式查询数据,可以声明一个新的索引,查询会自动使用最合适的那些索引。无需更改查询来利用新的索引。 (请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使添加应用程序新功能变得更加容易。
|
||||
如果想按新的方式查询数据,你可以声明一个新的索引,查询会自动使用最合适的那些索引。无需更改查询来利用新的索引。(请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使添加应用程序新功能变得更加容易。
|
||||
|
||||
关系数据库的查询优化器是复杂的,耗费多年的研究和开发精力【18】。关系模型的一个关键洞察是:只需构建一次查询优化器,随后使用该数据库的所有应用程序都可以从中受益。如果你没有查询优化器的话,那么为特定查询手动编写访问路径比编写通用优化器更容易——不过通用解决方案从长期看更好。
|
||||
关系数据库的查询优化器是复杂的,已耗费了多年的研究和开发精力【18】。关系模型的一个关键洞察是:只需构建一次查询优化器,随后使用该数据库的所有应用程序都可以从中受益。如果你没有查询优化器的话,那么为特定查询手动编写访问路径比编写通用优化器更容易——不过从长期看通用解决方案更好。
|
||||
|
||||
#### 与文档数据库相比
|
||||
|
||||
在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如`positions`,`education`和`contact_info`),而不是在单独的表中。
|
||||
|
||||
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为**外键**,在文档模型中称为**文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止,文档数据库没有遵循CODASYL的路数。
|
||||
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为**外键**,在文档模型中称为**文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止,文档数据库没有走CODASYL的老路。
|
||||
|
||||
### 关系型数据库与文档数据库在今日的对比
|
||||
|
||||
@ -225,25 +225,25 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
|
||||
|
||||
#### 哪个数据模型更方便写代码?
|
||||
|
||||
如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。
|
||||
如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的`positions`,`education`和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。
|
||||
|
||||
文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说“用户251的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,通常不是问题。
|
||||
文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说“用户251的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。
|
||||
|
||||
对文档数据库连接的糟糕支持也许或也许不是一个问题,这取决于应用程序。例如,分析应用程可能永远不需要多对多的关系,如果它使用文档数据库来记录何事发生于何时【19】。
|
||||
文档数据库对连接的糟糕支持也许或也许不是一个问题,这取决于应用程序。例如,分析应用程可能永远不需要多对多的关系,如果它使用文档数据库来记录何事发生于何时【19】。
|
||||
|
||||
但是,如果您的应用程序确实使用多对多关系,那么文档模型就没有那么吸引人了。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能【15】。
|
||||
但是,如果你的应用程序确实使用多对多关系,那么文档模型就没有那么吸引人了。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能【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) {
|
||||
@ -260,48 +260,48 @@ 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`,并在读取时再填充,就像使用文档数据库一样。
|
||||
|
||||
读时模式更具优势,当由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为:
|
||||
读时模式更具优势,当由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构时。例如,因为:
|
||||
|
||||
* 有许多不同类型的对象,将每种类型的对象放在自己的表中是不现实的。
|
||||
* 数据的结构由您无法控制且随时可能变化的外部系统决定。
|
||||
* 存在许多不同类型的对象,将每种类型的对象放在自己的表中是不现实的。
|
||||
* 数据的结构由外部系统决定。你无法控制外部系统且它随时可能变化。
|
||||
|
||||
在这样的情况下,模式的伤害远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,如果所有记录都具有相同的结构,那么模式就是记录并强制这种结构的有用机制。第四章将更详细地讨论模式和模式演化。
|
||||
在这样的情况下,模式的坏处远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,要是所有记录都具有相同的结构,那么模式是记录并强制这种结构的有效机制。第四章将更详细地讨论模式和模式演化。
|
||||
|
||||
#### 查询的数据局部性
|
||||
|
||||
文档通常以单个连续字符串形式存储,编码为JSON,XML或其二进制变体(如MongoDB的BSON)。如果您的应用程序经常需要访问整个文档(例如,将其渲染至网页),则此存储局部性会带来性能优势。如果将数据分割到多个表中(如[图2-1](img/fig2-1.png)所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
|
||||
文档通常以单个连续字符串形式进行存储,编码为JSON,XML或其二进制变体(如MongoDB的BSON)。如果应用程序经常需要访问整个文档(例如,将其渲染至网页),那么存储局部性会带来性能优势。如果将数据分割到多个表中(如[图2-1](img/fig2-1.png)所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
|
||||
|
||||
局部性仅适用于需要文档绝大部分内容的情况。数据库通常需要加载整个文档,即使只访问其中的一小部分,这对于大型文档来说是很浪费的。更新文档时,通常需要整个重写。 只有不改变文档大小的修改才可以容易地原地执行。因此通常建议,保持相对小的文档,并避免增加文档的大小的写入【9】。这些性能限制大大减少了文档数据库的实用场景。
|
||||
局部性仅仅适用于同时需要文档绝大部分内容的情况。数据库通常需要加载整个文档,即使只访问其中的一小部分,这对于大型文档来说是很浪费的。更新文档时,通常需要整个重写。只有不改变文档大小的修改才可以容易地原地执行。因此,通常建议保持相对小的文档,并避免增加文档大小的写入【9】。这些性能限制大大减少了文档数据库的实用场景。
|
||||
|
||||
值得指出的是,将相关的数据分组在一起的想法并不局限于文档模型。例如,Google的Spanner数据库在关系数据模型中提供了相同的位置属性,允许模式声明一个表的行应该在父表内**交织(interleaved)**(嵌套)【27】。 Oracle允许使用一个称为**多表索引集群表(multi-table index cluster tables)**的类似特性【28】。 Bigtable数据模型(在Cassandra和HBase中使用)中的**列族(column-family)**概念与管理局部性的目的类似【29】。
|
||||
值得指出的是,为了局部性而分组集合相关数据的想法并不局限于文档模型。例如,Google的Spanner数据库在关系数据模型中提供了同样的局部性属性,允许模式声明一个表的行应该交错(嵌套)在父表内【27】。Oracle类似地允许使用一个称为**多表索引集群表(multi-table index cluster tables)**的类似特性【28】。Bigtable数据模型(用于Cassandra和HBase)中的**列族(column-family)**概念与管理局部性的目的类似【29】。
|
||||
|
||||
在[第3章](ch3.md)我们还会看到更多关于本地的内容。
|
||||
在[第3章](ch3.md)将还会看到更多关于局部性的内容。
|
||||
|
||||
#### 文档和关系数据库的融合
|
||||
|
||||
自2000年代以来,大多数关系数据库系统(MySQL除外)都支持XML。这包括对XML文档进行本地修改的功能,以及在XML文档中进行索引和查询的功能,这允许应用程序使用与使用文档数据库时所做的非常相似的数据模型。
|
||||
自2000年代中期以来,大多数关系数据库系统(MySQL除外)都已支持XML。这包括对XML文档进行本地修改的功能,以及在XML文档中进行索引和查询的功能。这允许应用程序使用那种与文档数据库应当使用的非常类似的数据模型。
|
||||
|
||||
从9.3版本开始的PostgreSQL 【8】,从5.7版本开始的MySQL以及从版本10.5开始的IBM DB2 [30]也对JSON文档提供了类似的支持级别。鉴于Web API的JSON流行,其他关系数据库很可能会跟随他们的脚步并添加JSON支持。
|
||||
从9.3版本开始的PostgreSQL 【8】,从5.7版本开始的MySQL以及从版本10.5开始的IBM DB2 [30]也对JSON文档提供了类似的支持级别。鉴于用在Web APIs的JSON流行趋势,其他关系数据库很可能会跟随他们的脚步并添加JSON支持。
|
||||
|
||||
在文档数据库方面,RethinkDB支持其查询语言中的类似关系的连接,一些MongoDB驱动程序自动解决数据库引用(有效地执行客户端连接,尽管这可能比在数据库中执行的连接慢,需要额外的网络往返,并且没有优化)。
|
||||
在文档数据库中,RethinkDB在其查询语言中支持类似关系的连接,一些MongoDB驱动程序可以自动解析数据库引用(有效地执行客户端连接,尽管这可能比在数据库中执行的连接慢,需要额外的网络往返,并且优化更少)。
|
||||
|
||||
似乎随着时间的推移,关系数据库和文档数据库变得越来越相似,这是一件好事:数据模型相互补充[^v],如果一个数据库能够处理类似文档的数据,并对其执行关系查询,那么应用可以使用最适合其需要的特征的组合。关系模型和文档模型的混合是数据库未来的一个很好的途径。
|
||||
随着时间的推移,关系数据库和文档数据库似乎变得越来越相似,这是一件好事:数据模型相互补充[^v],如果一个数据库能够处理类似文档的数据,并能够对其执行关系查询,那么应用程序就可以使用最符合其需求的功能组合。
|
||||
|
||||
[^v]: Codd对关系模型【1】的原始描述实际上允许在关系模式中与JSON文档非常相似。 他称之为**非简单域(nonsimple domains)**。 这个想法是,一行中的值不一定是一个像数字或字符串一样的原始数据类型,也可以是一个嵌套的关系(表格)所以你可以有一个任意嵌套的树结构作为一个值, 很像30年后添加到SQL中的JSON或XML支持。
|
||||
关系模型和文档模型的混合是未来数据库一条很好的路线。
|
||||
|
||||
Codd对关系模型【1】的原始描述实际上允许在关系模式中与JSON文档非常相似。 他称之为**非简单域
|
||||
[^v]: Codd对关系模型【1】的原始描述实际上允许在关系模式中与JSON文档非常相似。他称之为**非简单域(nonsimple domains)**。这个想法是,一行中的值不一定是一个像数字或字符串一样的原始数据类型,也可以是一个嵌套的关系(表),因此可以把一个任意嵌套的树结构作为一个值,这很像30年后添加到SQL中的JSON或XML支持。
|
||||
|
||||
|
||||
|
||||
## 数据查询语言
|
||||
|
||||
当引入关系模型时,它包含了一种查询数据的新方法:SQL是一个声明式查询语言,而IMS和CODASYL使用命令式的代码来查询数据库。那是什么意思?
|
||||
当引入关系模型时,关系模型包含了一种查询数据的新方法:SQL是一种**声明式**查询语言,而IMS和CODASYL使用**命令式**代码来查询数据库。那是什么意思?
|
||||
|
||||
许多常用的编程语言是命令式的。例如,如果你有一个动物物种的列表,返回列表中的鲨鱼可以这样写:
|
||||
许多常用的编程语言是命令式的。例如,给定一个动物物种的列表,返回列表中的鲨鱼可以这样写:
|
||||
|
||||
```js
|
||||
function getSharks() {
|
||||
@ -321,29 +321,29 @@ sharks = σ_{family = "sharks"}(animals)
|
||||
$$
|
||||
σ(希腊字母西格玛)是选择操作符,只返回符合条件的动物,`family="shark"`。
|
||||
|
||||
在定义SQL时,它紧密地遵循关系代数的结构:
|
||||
定义SQL时,它紧密地遵循关系代数的结构:
|
||||
|
||||
```sql
|
||||
SELECT * FROM animals WHERE family ='Sharks';
|
||||
```
|
||||
|
||||
命令式语言告诉计算机以特定顺序执行某些操作。你可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。
|
||||
命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。
|
||||
|
||||
在声明式查询语言(如SQL或关系代数)中,您只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合)但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。
|
||||
在声明式查询语言(如SQL或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。
|
||||
|
||||
声明式查询语言是有吸引力的,因为它通常比命令式API更加简洁和容易。但更重要的是,它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在不需要对查询进行任何更改的情况下提高性能。
|
||||
声明式查询语言是迷人的,因为它通常比命令式API更加简洁和容易。但更重要的是,它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在无需对查询做任何更改的情况下进行性能提升。
|
||||
|
||||
例如,在本节开头所示的命令代码中,动物列表以特定顺序出现。如果数据库想要在场景后面回收未使用的磁盘空间,则可能需要移动记录,改变动物出现的顺序。数据库能否安全地执行,而不会中断查询?
|
||||
例如,在本节开头所示的命令代码中,动物列表以特定顺序出现。如果数据库想要在后台回收未使用的磁盘空间,则可能需要移动记录,这会改变动物出现的顺序。数据库能否安全地执行,而不会中断查询?
|
||||
|
||||
SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变。但是如果查询被写为命令式的代码,那么数据库就永远不能确定代码是否依赖于排序。 SQL在功能上更加有限的事实为数据库提供了更多自动优化的空间。
|
||||
SQL示例不确保任何特定的顺序,因此不在意顺序是否改变。但是如果查询用命令式的代码来写的话,那么数据库就永远不可能确定代码是否依赖于排序。SQL相当有限的功能性为数据库提供了更多自动优化的空间。
|
||||
|
||||
最后,声明性语言往往适合于并行执行。现在,通过增加更多的内核,CPU的速度会更快,而不是以比以前更高的时钟速度运行【31】。命令代码很难在多个内核和多个机器之间并行化,因为它指定了必须以特定顺序执行的指令。声明性语言在并行执行中获得更快的机会,因为它们仅指定结果的模式,而不是用于确定结果的算法。如果合适,数据库可以自由使用查询语言的并行实现【32】。
|
||||
最后,声明式语言往往适合并行执行。现在,CPU的速度通过内核的增加变得更快,而不是以比以前更高的时钟速度运行【31】。命令代码很难在多个内核和多个机器之间并行化,因为它指定了指令必须以特定顺序执行。声明式语言更具有并行执行的潜力,因为它们仅指定结果的模式,而不指定用于确定结果的算法。在适当情况下,数据库可以自由使用查询语言的并行实现【32】。
|
||||
|
||||
### Web上的声明式查询
|
||||
|
||||
声明式查询语言的优点不仅限于数据库。 为了说明这一点,我们在一个完全不同的环境中比较声明式和命令式的方法:一个Web浏览器。
|
||||
声明式查询语言的优势不仅限于数据库。为了说明这一点,让我们在一个完全不同的环境中比较声明式和命令式方法:一个Web浏览器。
|
||||
|
||||
假设你有一个关于海洋动物的网站。 用户当前正在查看鲨鱼页面,因此您将当前所选的导航项目“鲨鱼”标记为当前选中项目。
|
||||
假设你有一个关于海洋动物的网站。用户当前正在查看鲨鱼页面,因此你将当前所选的导航项目“鲨鱼”标记为当前选中项目。
|
||||
|
||||
```html
|
||||
<ul>
|
||||
@ -365,7 +365,7 @@ SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变
|
||||
</ul>
|
||||
```
|
||||
|
||||
现在想让当前所选页面的标题有一个蓝色的背景,以便在视觉上突出显示。 使用CSS实现起来非常简单:
|
||||
现在想让当前所选页面的标题具有一个蓝色的背景,以便在视觉上突出显示。使用CSS实现起来非常简单:
|
||||
|
||||
```css
|
||||
li.selected > p {
|
||||
@ -373,9 +373,9 @@ li.selected > p {
|
||||
}
|
||||
```
|
||||
|
||||
在这里,CSS选择器`li.selected> p`声明了我们想要应用蓝色样式的元素的模式:即直接父元素是一个CSS元素的`<li>`元素的所有`<p>`元素。 示例中的元素`<p> Sharks </ p>`匹配此模式,但`<p> Whales </ p>`不匹配,因为其`<li>`父类缺少`class =“selected”`。
|
||||
这里的CSS选择器`li.selected> p`声明了我们想要应用蓝色样式的元素的模式:即其直接父元素是具有`selected`CSS类的`<li>`元素的所有`<p>`元素。示例中的元素`<p> Sharks </p>`匹配此模式,但`<p> Whales </p>`不匹配,因为其`<li>`父元素缺少`class =“selected”`。
|
||||
|
||||
如果你使用XSL而不是CSS,你可以做类似的事情:
|
||||
如果使用XSL而不是CSS,你可以做类似的事情:
|
||||
|
||||
```xml
|
||||
<xsl:template match="li[@class='selected']/p">
|
||||
@ -385,9 +385,9 @@ li.selected > p {
|
||||
</xsl:template>
|
||||
```
|
||||
|
||||
在这里,XPath表达式`li[@class='selected']/p`相当于上例中的CSS选择器`li.selected> p`。 CSS和XSL的共同之处在于,它们都是用于指定文档样式的声明性语言。
|
||||
这里的XPath表达式`li[@class='selected']/p`相当于上例中的CSS选择器`li.selected> p`。CSS和XSL的共同之处在于,它们都是用于指定文档样式的声明式语言。
|
||||
|
||||
想象一下,如果你必须使用一命令式的方法,生活会是什么样子。在Javascript中,使用**文档对象模型(DOM)**API,结果可能如下所示:
|
||||
想象一下,必须使用命令式方法的情况会是如何。在Javascript中,使用**文档对象模型(DOM)**API,其结果可能如下所示:
|
||||
|
||||
```js
|
||||
var liElements = document.getElementsByTagName("li");
|
||||
@ -404,27 +404,27 @@ for (var i = 0; i < liElements.length; i++) {
|
||||
}
|
||||
```
|
||||
|
||||
这个JavaScript强制性地将元素设置为蓝色背景,但是代码太糟糕了。不仅比CSS和XSL等价物更长,更难理解,而且还有一些严重的问题:
|
||||
这段JavaScript代码命令式地将元素设置为蓝色背景,但是代码看起来很糟糕。不仅比CSS和XSL等价物更长,更难理解,而且还有一些严重的问题:
|
||||
|
||||
* 如果选定的类被移除(例如,因为用户点击了不同的页面),即使代码重新运行,蓝色也不会被移除 - 因此该项目将保持突出显示,直到整个页面被重新加载。使用CSS,浏览器会自动检测`li.selected> p`规则何时不再适用,并在选定的类被移除后立即移除蓝色背景。
|
||||
* 如果选定的类被移除(例如,因为用户点击了不同的页面),即使代码重新运行,蓝色背景也不会被移除 - 因此该项目将保持突出显示,直到整个页面被重新加载。使用CSS,浏览器会自动检测`li.selected> p`规则何时不再适用,并在选定的类被移除后立即移除蓝色背景。
|
||||
|
||||
* 如果你想要利用新的API(例如`document.getElementsBy ClassName(“selected”`)甚至`document.evaluate()`)来提高性能,则必须重写代码。另一方面,浏览器供应商可以在不破坏兼容性的情况下提高CSS和XPath的性能。
|
||||
|
||||
* 如果您想要利用新的API(例如`document.getElementsBy ClassName(“selected”`)甚至`document.evaluate()`)来提高性能,则必须重写代码。另一方面,浏览器供应商可以在不破坏兼容性的情况下提高CSS和XPath的性能。
|
||||
在Web浏览器中,使用声明式CSS样式比使用JavaScript命令式地操作样式要好得多。类似地,在数据库中,使用像SQL这样的声明式查询语言比使用命令式查询API要好得多[^vi]。
|
||||
|
||||
在Web浏览器中,使用声明式CSS样式比在JavaScript中强制操作样式好得多。类似地,在数据库中,像SQL这样的声明式查询语言比命令式查询API(IMS与CODASYL都是命令式的,应用程序通常通过COBOL代码来一次一行迭代数据库中的记录)要好得多[^vi]。
|
||||
|
||||
[^vi]: vi IMS和CODASYL都使用命令式API。 应用程序通常使用COBOL代码遍历数据库中的记录,一次一条记录【2,16】。
|
||||
[^vi]: vi IMS和CODASYL都使用命令式API。应用程序通常使用COBOL代码遍历数据库中的记录,一次一条记录【2,16】。
|
||||
|
||||
### MapReduce查询
|
||||
|
||||
MapReduce是一个编程模型,用于在许多机器上批量处理大量的数据,由Google推广【33】。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
|
||||
总的来说,MapReduce在[第10章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB对这一模型的应用。
|
||||
MapReduce是一个由Google推广的编程模型,用于在多台机器上批量处理大规模的数据【33】。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
|
||||
|
||||
MapReduce既不是一个声明性的查询语言,也不是一个完全强制性的查询API,而是位于两者之间的地方:查询的逻辑用代码片断来表示,这些代码片段被处理框架重复地调用。它基于许多函数式编程语言中存在的map(也称为collect)和reduce(也称为fold或inject)函数。
|
||||
MapReduce将[第10章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB使用的模型。
|
||||
|
||||
举一个例子,假设你是一名海洋生物学家,每当你看到海洋中的动物时,你都会在数据库中添加观察记录。现在你想生成一个报告,说明你每月看到多少鲨鱼。
|
||||
MapReduce既不是一个声明式的查询语言,也不是一个完全命令式的查询API,而是处于两者之间:查询的逻辑用代码片断来表示,这些代码片段会被处理框架重复性调用。它基于`map`(也称为`collect`)和`reduce`(也称为`fold`或`inject`)函数,两个函数存在于许多函数式编程语言中。
|
||||
|
||||
在PostgreSQL中,你可以像这样表达这个查询:
|
||||
最好举例来解释MapReduce模型。假设你是一名海洋生物学家,每当你看到海洋中的动物时,你都会在数据库中添加一条观察记录。现在你想生成一个报告,说明你每月看到多少鲨鱼。
|
||||
|
||||
在PostgreSQL中,你可以像这样表述这个查询:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
@ -435,11 +435,11 @@ WHERE family = 'Sharks'
|
||||
GROUP BY observation_month;
|
||||
```
|
||||
|
||||
`date_trunc('month',timestamp)`函数确定包含时间戳记的日历月份,并返回代表该月份开始的另一个时间戳记。换句话说,它将时间戳降到最近的月份。
|
||||
`date_trunc('month',timestamp)`函数用于确定包含`timestamp`的日历月份,并返回代表该月份开始的另一个时间戳。换句话说,它将时间戳舍入成最近的月份。
|
||||
|
||||
这个查询首先过滤观察结果,只显示鲨鱼家族的物种,然后根据它们发生的日历月份对观察结果进行分组,最后将在该月的所有观察中看到的动物数加起来。
|
||||
这个查询首先过滤观察记录,以只显示鲨鱼家族的物种,然后根据它们发生的日历月份对观察记录果进行分组,最后将在该月的所有观察记录中看到的动物数目加起来。
|
||||
|
||||
MongoDB的MapReduce功能也可以这样表示:
|
||||
同样的查询用MongoDB的MapReduce功能可以按如下来表述:
|
||||
|
||||
```js
|
||||
db.observations.mapReduce(function map() {
|
||||
@ -458,14 +458,14 @@ db.observations.mapReduce(function map() {
|
||||
});
|
||||
```
|
||||
|
||||
* 可以声明性地指定只考虑鲨鱼种类的过滤器(这是对MapReduce的特定于MongoDB的扩展)。
|
||||
* 可以声明式地指定只考虑鲨鱼种类的过滤器(这是一个针对MapReduce的特定于MongoDB的扩展)。
|
||||
* 每个匹配查询的文档都会调用一次JavaScript函数`map`,将`this`设置为文档对象。
|
||||
* `map`函数发出一个键(包括年份和月份的字符串,如`"2013-12"`或`"2014-1"`)和一个值(该观察中的动物数量)。
|
||||
* `map`发出的键值对按键组合。 对于具有相同键(即,相同的月份和年份)的所有键值对,`reduce`函数被调用一次。
|
||||
* `reduce`函数将特定月份内所有观测值的动物数量相加。
|
||||
* 最终的输出被写入到`monthlySharkReport`集合中。
|
||||
* `map`函数发出一个键(包括年份和月份的字符串,如`"2013-12"`或`"2014-1"`)和一个值(该观察记录中的动物数量)。
|
||||
* `map`发出的键值对按键来分组。对于具有相同键(即,相同的月份和年份)的所有键值对,调用一次`reduce`函数。
|
||||
* `reduce`函数将特定月份内所有观测记录中的动物数量相加。
|
||||
* 将最终的输出写入到`monthlySharkReport`集合中。
|
||||
|
||||
例如假设`observations`集合包含这两个文档:
|
||||
例如,假设`observations`集合包含这两个文档:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -482,15 +482,15 @@ db.observations.mapReduce(function map() {
|
||||
}
|
||||
```
|
||||
|
||||
`map`函数会对这两个文档每个调用一次,导致 `emit("1995-12",3)`和`emit("1995-12",4)`,然后,`reduce`函数会执行`reduce("1995-12",[3,4])`,并返回`7`。
|
||||
对每个文档都会调用一次`map`函数,结果将是`emit("1995-12",3)`和`emit("1995-12",4)`。随后,以`reduce("1995-12",[3,4])`调用`reduce`函数,将返回`7`。
|
||||
|
||||
map和reduce函数在功能上有限制:它们必须是纯函数,这意味着它们只使用传递给它们的数据作为输入,它们不能执行额外的数据库查询,也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能,并在失败时重新运行它们。然而,它们仍然是强大的:它们可以解析字符串,调用库函数,执行计算等等。
|
||||
map和reduce函数在功能上有所限制:它们必须是**纯**函数,这意味着它们只使用传递给它们的数据作为输入,它们不能执行额外的数据库查询,也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能,并在失败时重新运行它们。然而,map和reduce函数仍然是强大的:它们可以解析字符串,调用库函数,执行计算等等。
|
||||
|
||||
MapReduce是一个相当低级的编程模型,用于在一组机器上进行分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现(见[第10章](ch10.md)),但是也有很多不使用MapReduce的分布式SQL实现。请注意,SQL中没有任何内容限制它在单个机器上运行,而MapReduce在分布式查询执行方面没有垄断权。
|
||||
MapReduce是一个相当底层的编程模型,用于计算机集群上的分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现(见[第10章](ch10.md)),但是也有很多不使用MapReduce的分布式SQL实现。请注意,SQL中没有任何内容限制它在单个机器上运行,而MapReduce在分布式查询执行上没有垄断权。
|
||||
|
||||
能够在查询中使用JavaScript代码是高级查询的一个重要特性,但不限于MapReduce,一些SQL数据库也可以使用JavaScript函数进行扩展【34】。
|
||||
能够在查询中使用JavaScript代码是高级查询的一个重要特性,但这不限于MapReduce,一些SQL数据库也可以用JavaScript函数进行扩展【34】。
|
||||
|
||||
MapReduce的可用性问题是,你必须编写两个协调的JavaScript函数,这通常比编写单个查询更困难。此外,声明性查询语言为查询优化器提供了更多机会来提高查询的性能。由于这些原因,MongoDB 2.2添加了对称为聚合管道的声明性查询语言的支持【9】。在这种语言中,相同的鲨鱼计数查询如下所示:
|
||||
MapReduce的一个可用性问题是,必须编写两个密切合作的JavaScript函数,这通常比编写单个查询更困难。此外,声明式查询语言为查询优化器提供了更多机会来提高查询的性能。基于这些原因,MongoDB 2.2添加了一种叫做**聚合管道**的声明式查询语言的支持【9】。用这种语言表述鲨鱼计数查询如下所示:
|
||||
|
||||
```js
|
||||
db.observations.aggregate([
|
||||
@ -504,17 +504,17 @@ db.observations.aggregate([
|
||||
]);
|
||||
```
|
||||
|
||||
聚合流水线语言与SQL的子集表现类似,但是它使用基于JSON的语法而不是SQL的英语句子式语法; 这种差异也许是品味的问题。 这个故事的寓意是NoSQL系统可能会发现自己意外地重新发明了SQL,尽管是伪装的。
|
||||
聚合管道语言与SQL的子集具有类似表现力,但是它使用基于JSON的语法而不是SQL的英语句子式语法; 这种差异也许是口味问题。这个故事的寓意是NoSQL系统可能会发现自己意外地重新发明了SQL,尽管带着伪装。
|
||||
|
||||
|
||||
|
||||
## 图数据模型
|
||||
|
||||
如我们之前所见,多对多关系是不同数据模型之间的重要区别特征。如果您的应用程序大多数是一对多关系(树状结构化数据),或者记录之间没有关系,则文档模型是适当的。
|
||||
如我们之前所见,多对多关系是不同数据模型之间具有区别性的重要特征。如果你的应用程序大多数的关系是一对多关系(树状结构化数据),或者大多数记录之间不存在关系,那么使用文档模型是合适的。
|
||||
|
||||
但是,如果多对多的关系在您的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,开始将数据建模为图形变得更加自然。
|
||||
但是,要是多对多关系在你的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,将数据建模为图形显得更加自然。
|
||||
|
||||
一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)**或**弧 (arcs)** )。许多种数据可以被模拟为一个图形。典型的例子包括:
|
||||
一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)**或**弧 (arcs)** )。多种数据可以被建模为一个图形。典型的例子包括:
|
||||
|
||||
***社交图谱***
|
||||
|
||||
@ -522,15 +522,15 @@ db.observations.aggregate([
|
||||
|
||||
***网络图谱***
|
||||
|
||||
顶点是网页,边缘表示到其他页面的HTML链接。
|
||||
顶点是网页,边缘表示指向其他页面的HTML链接。
|
||||
|
||||
***公路或铁路网络***
|
||||
|
||||
顶点是连接点,边线代表它们之间的道路或铁路线。
|
||||
顶点是交叉路口,边线代表它们之间的道路或铁路线。
|
||||
|
||||
众所周知的算法可以在这些图上进行操作:例如,汽车导航系统搜索道路网络中两点之间的最短路径,PageRank可以用在网络图上来确定网页的流行程度,从而其在搜索结果中的排名。
|
||||
可以将那些众所周知的算法运用到这些图上:例如,汽车导航系统搜索道路网络中两点之间的最短路径,PageRank可以用在网络图上来确定网页的流行程度,从而确定该网页在搜索结果中的排名。
|
||||
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的东西(人,网页或交叉路口)。然而,图并不局限于这样的同类数据:图的同样强大的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方式。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示用户所做的人,地点,事件,签到和评论;边缘表示哪些人是彼此的朋友,哪个位置发生了检查,谁评论了哪个职位,谁参加了哪个事件,等等【35】。
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的事物(人,网页或交叉路口)。不过,图并不局限于这样的同类数据:同样强大地是,图提供了一种一致的方式,用来在单个数据存储中存储完全不同类型的对象。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示人,地点,事件,签到和用户的评论;边缘表示哪些人是彼此的朋友,哪个签到发生在何处,谁评论了哪条消息,谁参与了哪个事件,等等【35】。
|
||||
|
||||
在本节中,我们将使用[图2-5](img/fig2-5.png)所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚,住在伦敦。
|
||||
|
||||
@ -538,26 +538,26 @@ db.observations.aggregate([
|
||||
|
||||
**图2-5 图数据结构示例(框代表顶点,箭头代表边)**
|
||||
|
||||
有几种不同但相关的方法来构建和查询图表中的数据。 在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三元组存储(triple-store)模型(由Datomic,AllegroGraph等实现)。 我们将看图的三种声明性查询语言:Cypher,SPARQL和Datalog。 除此之外,还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架(见[第10章](ch10.md))。
|
||||
有几种不同但相关的方法用来构建和查询图表中的数据。在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三元组存储(triple-store)模型(由Datomic,AllegroGraph等实现)。我们将查看图的三种声明式查询语言:Cypher,SPARQL和Datalog。除此之外,还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架(见[第10章](ch10.md))。
|
||||
|
||||
### 属性图
|
||||
|
||||
在属性图模型中,每个**顶点(vertex)**包括:
|
||||
|
||||
* 唯一的标识符
|
||||
* **出向边集合(outgoing edges)**
|
||||
* **入向边集合(ingoing edges)**
|
||||
* 一组**出边(outgoing edges)**
|
||||
* 一组**入边(ingoing edges)**
|
||||
* 一组属性(键值对)
|
||||
|
||||
每条**边(edge)**包括:
|
||||
|
||||
* 唯一标识符
|
||||
* **边的起点/尾点(tail vertex)**
|
||||
* **边的终点/头点(head vertex)**
|
||||
* **边的起点/尾部顶点(tail vertex)**
|
||||
* **边的终点/头部顶点(head vertex)**
|
||||
* 描述两个顶点之间关系类型的标签
|
||||
* 一组属性(键值对)
|
||||
|
||||
可以将图存储看作两个关系表:一个存储顶点,另一个存储边,如[例2-2]()所示(该模式使用PostgreSQL json数据类型来存储每个顶点或边的属性)。头部和尾部顶点存储为每个边缘。如果您想要一组顶点的输入或输出边,您可以分别通过`head_vertex`或`tail_vertex`来查询边表。
|
||||
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边,如[例2-2]()所示(该模式使用PostgreSQL json数据类型来存储每个顶点或每条边的属性)。头部和尾部顶点用来存储每条边;如果你想要一组顶点的输入或输出边,你可以分别通过`head_vertex`或`tail_vertex`来查询`edges`表。
|
||||
|
||||
**例2-2 使用关系模式来表示属性图**
|
||||
|
||||
@ -579,21 +579,21 @@ CREATE INDEX edges_tails ON edges (tail_vertex);
|
||||
CREATE INDEX edges_heads ON edges (head_vertex);
|
||||
```
|
||||
|
||||
这个模型的一些重要方面是:
|
||||
关于这个模型的一些重要方面是:
|
||||
|
||||
1. 任何顶点都可以有一个边连接到任何其他顶点。没有哪种事物可不可以关联的模式限制。
|
||||
2. 给定任何顶点,您可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。 (这就是为什么[例2-2]()在`tail_vertex`和`head_vertex`列上都有索引的原因。)
|
||||
3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个干净的数据模型。
|
||||
1. 任何顶点都可以有一条边连接到任何其他顶点。没有模式限制哪种事物可不可以关联。
|
||||
2. 给定任何顶点,可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。(这就是为什么[例2-2]()在`tail_vertex`和`head_vertex`列上都有索引的原因。)
|
||||
3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个清晰的数据模型。
|
||||
|
||||
这些特性为数据建模提供了很大的灵活性,如[图2-5](img/fig2-5.png)所示。图中显示了一些传统关系模式难以表达的东西,例如不同国家的不同地区结构(法国有省和州,美国有不同的州和州),国中国的怪事(先忽略主权国家和国家错综复杂的烂摊子),不同的数据粒度(露西现在的住所被指定为一个城市,而她的出生地点只是在一个州的级别)。
|
||||
这些特性为数据建模提供了很大的灵活性,如[图2-5](img/fig2-5.png)所示。图中显示了一些传统关系模式难以表达的事情,例如不同国家的不同地区结构(法国有省和州,美国有不同的州和州),国中国的怪事(先忽略主权国家和国家错综复杂的烂摊子),不同的数据粒度(Lucy现在的住所被指定为一个城市,而她的出生地点只是在一个州的级别)。
|
||||
|
||||
你可以想象延伸图还包括许多关于露西和阿兰,或其他人的其他事实。例如,您可以用它来表示食物过敏(通过为每个过敏原引入一个顶点,以及人与过敏原之间的边缘来指示过敏),并将过敏原与一组过敏原显示哪些食物含有哪些物质的顶点。然后,你可以写一个查询,找出每个人吃什么是安全的。图表对于可演化性是有利的:当您向应用程序添加功能时,可以轻松扩展图形以适应应用程序数据结构的变化。
|
||||
你可以想象延伸图还能包括许多关于Lucy和Alain,或其他人的其他更多的事实。例如,你可以用它来表示食物过敏(为每个过敏源增加一个顶点,并增加人与过敏源之间的一条边来指示一种过敏情况),并链接到过敏源,每个过敏源具有一组顶点用来显示哪些食物含有哪些物质。然后,你可以写一个查询,找出每个人吃什么是安全的。图表在可演化性是富有优势的:当向应用程序添加功能时,可以轻松扩展图以适应应用程序数据结构的变化。
|
||||
|
||||
### Cypher查询语言
|
||||
|
||||
Cypher是属性图的声明式查询语言,为Neo4j图形数据库发明【37】。 (它是以电影“黑客帝国”中的角色命名的,与密码术中的密码无关【38】。)
|
||||
Cypher是属性图的声明式查询语言,为Neo4j图形数据库而发明【37】。(它是以电影“黑客帝国”中的一个角色开命名的,而与密码术中的密码无关【38】。)
|
||||
|
||||
[例2-3]()显示了将[图2-5](img/fig2-5.png)的左边部分插入图形数据库的Cypher查询。图的其余部分可以类似地添加,为了便于阅读而省略。每个顶点都有一个像USA或Idaho这样的符号名称,查询的其他部分可以使用这些名称在顶点之间创建边,使用箭头符号:`(Idaho) - [:WITHIN] ->(USA)`创建一个带有标记`WITHIN`的边,爱达荷州为尾节点,美国为头节点。
|
||||
[例2-3]()显示了将[图2-5](img/fig2-5.png)的左边部分插入图形数据库的Cypher查询。可以类似地添加图的其余部分,为了便于阅读而省略。每个顶点都有一个像`USA`或`Idaho`这样的符号名称,查询的其他部分可以使用这些名称在顶点之间创建边,使用箭头符号:`(Idaho) - [:WITHIN] ->(USA)`创建一条标记为`WITHIN`的边,`Idaho`为尾节点,`USA`为头节点。
|
||||
|
||||
**例2-3 将图2-5中的数据子集表示为Cypher查询**
|
||||
|
||||
@ -607,9 +607,11 @@ CREATE
|
||||
(Lucy) -[:BORN_IN]-> (Idaho)
|
||||
```
|
||||
|
||||
当[图2-5](img/fig2-5.png)的所有顶点和边被添加到数据库时,我们可以开始提出有趣的问题:例如,找到所有从美国移民到欧洲的人的名字。更确切地说,在这里我们想要找到在美国有一个`BORN_IN`边缘的所有顶点,还有一个`LIVING_IN`边缘到欧洲的一个位置,并且返回每个这些顶点的名称属性。
|
||||
当[图2-5](img/fig2-5.png)的所有顶点和边被添加到数据库后,让我们提些有趣的问题:例如,找到所有从美国移民到欧洲的人的名字。更确切地说,这里我们想要找到符合下面条件的所有顶点,并且返回这些顶点的`name`属性:该顶点拥有一条连到美国任一位置的`BORN_IN`边,和一条连到欧洲的任一位置的`LIVING_IN`边。
|
||||
|
||||
**例2-4 展示了如何在Cypher中表达这个查询。在MATCH子句中使用相同的箭头符号来查找图中的模式:**
|
||||
[例2-4]()展示了如何在Cypher中表达这个查询。在MATCH子句中使用相同的箭头符号来查找图中的模式:`(person) -[:BORN_IN]-> ()` 可以匹配`BORN_IN`边的任意两个顶点。该边的尾节点被绑定了变量`person`,头节点则未被绑定。
|
||||
|
||||
**例2-4 查找所有从美国移民到欧洲的人的Cypher查询:**
|
||||
|
||||
```cypher
|
||||
MATCH
|
||||
@ -618,35 +620,38 @@ MATCH
|
||||
RETURN person.name
|
||||
```
|
||||
|
||||
查询可以被读取如下:
|
||||
查询按如下来解读:
|
||||
|
||||
> 找到满足以下两者的所有顶点(称之为person):
|
||||
> 1. 有一条到某个顶点`BORN_IN`类型的出边。从那个顶点开始可以沿着一系列`WITHIN`出边最终到达类型为`Location`,`name=United States`的顶点
|
||||
> 找到满足以下两个条件的所有顶点(称之为person顶点):
|
||||
> 1. `person`顶点拥有一条到某个顶点的`BORN_IN`出边。从那个顶点开始,沿着一系列`WITHIN`出边最终到达一个类型为`Location`,`name`属性为`United States`的顶点。
|
||||
>
|
||||
> 2. `person`顶点还拥有一条`LIVES_IN`出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达一个类型为`Location`,`name`属性为`Europe`的顶点。
|
||||
>
|
||||
> 2. 也有一条到某个顶点`LIVES_IN`类型的出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达类型为`Location`,`name=Europe`的顶点
|
||||
>
|
||||
> 对于这样的`Person`类型顶点,返回其`name`属性。
|
||||
> 对于这样的`Person`顶点,返回其`name`属性。
|
||||
|
||||
这条查询有几种可行的查询路径。这里给出的描述建议你首先扫描数据库中的所有人,检查每个人的出生地和居住地,然后只返回符合条件的人。
|
||||
执行这条查询可能会有几种可行的查询路径。这里给出的描述建议首先扫描数据库中的所有人,检查每个人的出生地和居住地,然后只返回符合条件的那些人。
|
||||
|
||||
等价地,也可以从两个位置顶点开始并向后查找。如果名称属性上有一个索引,则可以高效地找到代表美国和欧洲的两个顶点。然后,您可以继续查找所有在`WITHIN`边中的位置(美国和欧洲的所有位置(州,地区,城市等))。最后,您可以查找可以通过在某个位置顶点处传入的`BORN_IN`或`LIVES_IN`边找到的人员。
|
||||
等价地,也可以从两个`Location`顶点开始反向地查找。假如`name`属性上有索引,则可以高效地找到代表美国和欧洲的两个顶点。然后,沿着所有`WITHIN`入边,可以继续查找出所有在美国和欧洲的位置(州,地区,城市等)。最后,查找出那些可以由`BORN_IN`或`LIVES_IN`入边到那些位置顶点的人。
|
||||
|
||||
对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。
|
||||
通常对于声明式查询语言来说,在编写查询语句时,不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此你可以继续编写应用程序的其他部分。
|
||||
|
||||
### SQL中的图查询
|
||||
|
||||
[例2-2]()建议可以在关系数据库中表示图数据。但是,如果我们把图数据放入关系结构中,我们是否也可以使用SQL查询它?答案是肯定的,但有些困难。在关系数据库中,您通常会事先知道在查询中需要哪些连接。在图查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。
|
||||
[例2-2]()建议在关系数据库中表示图数据。但是,如果把图数据放入关系结构中,我们是否也可以使用SQL查询它?
|
||||
|
||||
在我们的例子中,这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。
|
||||
在Cypher中,`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个`WITHIN`边,零次或多次”。它就像正则表达式中的`*`运算符。
|
||||
由于SQL:1999,查询中可变长度遍历路径的思想可以使用称为递归公用表表达式(`WITH RECURSIVE`语法)的东西来表示。[例2-5]()显示了同样的查询 - 查找使用这种技术(PostgreSQL,IBM DB2,Oracle和SQL Server支持)中从美国移民到欧洲的人的姓名。但是,与Cypher相比,语法非常笨拙。
|
||||
答案是肯定的,但有些困难。在关系数据库中,你通常会事先知道在查询中需要哪些连接。在图查询中,你可能需要在找到待查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。
|
||||
|
||||
**例2-5 与示例2-4相同的查询,使用递归CTE表达式在SQL中表示**
|
||||
在我们的例子中,这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向正在查找的位置,或者一个在位置层次结构中隔了数层的位置。
|
||||
|
||||
在Cypher中,用`WITHIN * 0`非常简洁地表述了上述事实:“沿着`WITHIN`边,零次或多次”。它很像正则表达式中的`*`运算符。
|
||||
|
||||
自SQL:1999,查询可变长度遍历路径的思想可以使用称为**递归公用表表达式**(`WITH RECURSIVE`语法)的东西来表示。[例2-5]()显示了同样的查询 - 查找从美国移民到欧洲的人的姓名 - 在SQL使用这种技术(PostgreSQL,IBM DB2,Oracle和SQL Server均支持)来表述。但是,与Cypher相比,其语法非常笨拙。
|
||||
|
||||
**例2-5 与示例2-4同样的查询,在SQL中使用递归公用表表达式表示**
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE
|
||||
-- in_usa 包含所有的美国境内的地点ID
|
||||
-- in_usa 包含所有的美国境内的位置ID
|
||||
in_usa(vertex_id) AS (
|
||||
SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'United States'
|
||||
UNION
|
||||
@ -654,7 +659,7 @@ WITH RECURSIVE
|
||||
JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
|
||||
WHERE edges.label = 'within'
|
||||
),
|
||||
-- in_europe 包含所有的欧洲境内的地点ID
|
||||
-- in_europe 包含所有的欧洲境内的位置ID
|
||||
in_europe(vertex_id) AS (
|
||||
SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'Europe'
|
||||
UNION
|
||||
@ -680,24 +685,25 @@ WITH RECURSIVE
|
||||
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;
|
||||
```
|
||||
|
||||
* 集合`in_usa`以(`Location name=United States`)作为种子,沿着边表,从种子集作为起点,将所有具有`with_in`边的终点加入种子集,不断递归直到边表内的所有条目都被访问完毕。
|
||||
* 同理建立点集`in_europe`,获取欧洲的地点列表。
|
||||
* 对于`in_usa`集合中的每个顶点,按照传入的`born_in`边缘来查找出生在美国某个地方的人。
|
||||
* 同样,对于`in_europe`集合中的每个顶点,请按照传入的`lives_in`边来查找居住在欧洲的人。
|
||||
* 最后,把在美国出生的人与在欧洲居住的人相交,获取他们的名称。
|
||||
* 首先,查找`name`属性为`United States`的顶点,将其作为`in_use`顶点的集合的第一个元素。
|
||||
* 从`in_use`集合的顶点出发,沿着所有的`with_in`入边,将其尾顶点加入同一集合,不断递归直到所有`with_in`入边都被访问完毕。
|
||||
* 同理,从`name`属性为`Europe`的顶点出发,建立`in_europe`顶点的集合。
|
||||
* 对于`in_usa`集合中的每个顶点,根据`born_in`入边来查找出生在美国某个地方的人。
|
||||
* 同样,对于`in_europe`集合中的每个顶点,根据`lives_in`入边来查找居住在欧洲的人。
|
||||
* 最后,把在美国出生的人的集合与在欧洲居住的人的集合相交。
|
||||
|
||||
同一个查询,可以用一个查询语言写成4行,而另一个查询需要29行,这说明了不同的数据模型是为不同的应用场景设计的。选择适合应用程序的数据模型非常重要。
|
||||
同一个查询,用某一个查询语言可以写成4行,而用另一个查询语言需要29行,这恰恰说明了不同的数据模型是为不同的应用场景而设计的。选择适合应用程序的数据模型非常重要。
|
||||
|
||||
### 三元组存储和SPARQL
|
||||
|
||||
三元组存储模式大体上与属性图模型相同,用不同的词来描述相同的想法。不过值得讨论的是,因为三元组存储有很多现成的工具和语言,这些工具和语言对于构建应用程序的工具箱可能是有价值的补充。
|
||||
三元组存储模式大体上与属性图模型相同,用不同的词来描述相同的想法。不过仍然值得讨论,因为三元组存储有很多现成的工具和语言,这些工具和语言对于构建应用程序的工具箱可能是宝贵的补充。
|
||||
|
||||
在三元组存储中,所有信息都以非常简单的三部分表示形式存储(主体,谓词,客体)。例如,在三重(吉姆,喜欢,香蕉),吉姆是主语,喜欢是谓语(动词),香蕉是对象。
|
||||
在三元组存储中,所有信息都以非常简单的三部分表示形式存储(**主语**,**谓语**,**宾语**)。例如,三元组**(吉姆, 喜欢 ,香蕉)**中,**吉姆**是主语,**喜欢**是谓语(动词),**香蕉**是对象。
|
||||
|
||||
三元组的主体相当于图中的一个顶点。而客体是两件事情之一:
|
||||
三元组的主语相当于图中的一个顶点。而宾语是下面两者之一:
|
||||
|
||||
1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓词和对象相当于主题顶点上的属性的键和值。例如,`(lucy, age, 33)`就像属性`{“age”:33}`的顶点lucy。
|
||||
2. 图中的另一个顶点。在这种情况下,谓词是图中的边,主体是尾部顶点,而对象是顶点。例如,在`(lucy, marriedTo, alain)`主语和宾语`lucy`和`alain`都是顶点,并且谓词`marriedTo`是连接他们的边的标签。
|
||||
1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓语和宾语相当于主语顶点上的属性的键和值。例如,`(lucy, age, 33)`就像属性`{“age”:33}`的顶点lucy。
|
||||
2. 图中的另一个顶点。在这种情况下,谓语是图中的一条边,主语是其尾部顶点,而宾语是其头部顶点。例如,在`(lucy, marriedTo, alain)`中主语和宾语`lucy`和`alain`都是顶点,并且谓语`marriedTo`是连接他们的边的标签。
|
||||
|
||||
[例2-6]()显示了与[例2-3]()相同的数据,以称为Turtle的格式(Notation3(N3)【39】)的一个子集形式写成三元组。
|
||||
|
||||
@ -721,11 +727,11 @@ _:namerica :name "North America"
|
||||
_:namerica :type :"continent"
|
||||
```
|
||||
|
||||
在这个例子中,图的顶点被写为:`_:someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确三元组之间的相互引用。当谓词表示边时,该对象是一个顶点,如:`_:idaho :within _:usa.`。当谓词是一个属性时,该对象是一个字符串,如:`_:usa :name "United States"`
|
||||
在这个例子中,图的顶点被写为:`_:someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确哪些三元组引用了同一顶点。当谓语表示边时,该宾语是一个顶点,如`_:idaho :within _:usa.`。当谓语是一个属性时,该宾语是一个字符串,如`_:usa :name "United States"`
|
||||
|
||||
一遍又一遍地重复相同的主题是相当重复的,但幸运的是,您可以使用分号来说明关于同一主题的多个事情。这使得Turtle格式相当不错,可读性强:参见[例2-7]()。
|
||||
一遍又一遍地重复相同的主语看起来相当重复,但幸运的是,可以使用分号来说明关于同一主语的多个事情。这使得Turtle格式相当不错,可读性强:参见[例2-7]()。
|
||||
|
||||
**例2-7 在示例2-6 `@prefix:<urn:example:>`中写入数据的更简洁的方法。**
|
||||
**例2-7 一种相对例2-6写入数据的更为简洁的方法。**
|
||||
|
||||
```
|
||||
@prefix : <urn:example:>.
|
||||
@ -737,21 +743,21 @@ _:namerica a :Location; :name "North America"; :type "continent".
|
||||
|
||||
#### 语义网络
|
||||
|
||||
如果您阅读更多关于三元组存储的信息,您可能会被卷入关于语义网的文章中。三元组存储数据模型完全独立于语义网络,例如,Datomic 【40】是三元组存储[^vii],并不声称与它有任何关系。但是,由于在很多人眼中这两者紧密相连,我们应该简要地讨论一下。
|
||||
如果你阅读更多关于三元组存储的信息,你可能会被卷入关于语义网络的文章漩涡中。三元组存储数据模型完全独立于语义网络,例如,Datomic【40】是三元组存储[^vii],并没有声称与它有任何关系。但是,由于在很多人眼中这两者紧密相连,我们应该简要地讨论一下。
|
||||
|
||||
[^vii]: 从技术上讲,Datomic使用的是五元组而不是三元组,两个额外的字段是用于版本控制的元数据
|
||||
|
||||
语义网从本质上讲是一个简单而合理的想法:网站已经将信息发布为文字和图片供人类阅读,为什么不把它们作为机器可读的数据发布给计算机呢?资源描述框架(RDF)【41】的目的是作为不同网站以一致的格式发布数据的一种机制,允许来自不同网站的数据自动合并成一个数据网络 - 一种互联网范围内的“一切的数据库“。
|
||||
从本质上讲语义网是一个简单且合理的想法:网站已经将信息发布为文字和图片供人类阅读,为什么不将信息作为机器可读的数据也发布给计算机呢?**资源描述框架**(RDF)【41】的目的是作为不同网站以一致的格式发布数据的一种机制,允许来自不同网站的数据自动合并成**一个数据网络** - 一种互联网范围内的“关于一切的数据库“。
|
||||
|
||||
不幸的是,这个语义网在二十一世纪初被过度使用,但到目前为止还没有显示出在实践中有任何实现的迹象,这使得许多人愤世嫉俗。它也遭受了令人眼花缭乱的缩略词,过于复杂的标准提议和自大。
|
||||
不幸的是,这个语义网在二十一世纪初被过度使用,但到目前为止没有任何迹象表明已在实践中实现,这使得许多人呲之以鼻。它还遭受了过多的令人眼花缭乱的缩略词,过于复杂的标准提议和狂妄自大的苦果。
|
||||
|
||||
但是,如果你仔细观察这些失败,那么语义Web项目中也有很多好的工作。即使您没有兴趣在语义网上发布RDF数据,三元组也可以成为应用程序的良好内部数据模型。
|
||||
然而,如果仔细观察这些失败,语义Web项目还是拥有很多优秀的工作成果。即使你没有兴趣在语义网上发布RDF数据,三元组也可以成为应用程序的良好内部数据模型。
|
||||
|
||||
#### RDF数据模型
|
||||
|
||||
[例2-7]()中使用的Turtle语言是RDF数据的可读格式。有时候,RDF也是以XML格式编写的,它可以更详细地完成同样的事情,参见[例2-8]()。Turtle N3是最好的,因为它更容易阅读,像Apache Jena 【42】这样的工具可以根据需要在不同的RDF格式之间自动转换。
|
||||
[例2-7]()中使用的Turtle语言是一种用于RDF数据的人可读格式。有时候,RDF也可以以XML格式编写,不过完成同样的事情会相对啰嗦,参见[例2-8]()。Turtle/N3是更可取的,因为它更容易阅读,像Apache Jena 【42】这样的工具可以根据需要在不同的RDF格式之间进行自动转换。
|
||||
|
||||
**例2-8 例2-7的数据,用RDF/XML语法表示**
|
||||
**例2-8 用RDF/XML语法表示例2-7的数据**
|
||||
|
||||
```xml
|
||||
<rdf:RDF xmlns="urn:example:"
|
||||
@ -779,14 +785,15 @@ _:namerica a :Location; :name "North America"; :type "continent".
|
||||
</rdf:RDF>
|
||||
```
|
||||
|
||||
RDF有一些奇怪之处,因为它是为了在互联网上交换数据而设计的。三元组的主题,谓词和对象通常是URI。例如,谓词可能是一个URI,如 `<http://my-company.com/namespace#within>`或`<http://my-company.com/namespace#lives_in>`,而不仅仅是WITHIN或LIVES_IN。这个设计背后的原因是你应该能够把你的数据和其他人的数据结合起来,如果他们给这个单词或者lives_in附加不同的含义,你不会得到冲突,因为它们的谓词实际上是`<http://other.org/foo#within>`和`<http://other.org/foo#lives_in>`。
|
||||
RDF有一些奇怪之处,因为它是为了在互联网上交换数据而设计的。三元组的主语,谓语和宾语通常是URI。例如,谓语可能是一个URI,如 `<http://my-company.com/namespace#within>`或`<http://my-company.com/namespace#lives_in>`,而不仅仅是`WITHIN`或`LIVES_IN`。这个设计背后的原因为了让你能够把你的数据和其他人的数据结合起来,如果他们赋予单词`within`或者`lives_in`不同的含义,两者也不会冲突,因为它们的谓语实际上是`<http://other.org/foo#within>`和`<http://other.org/foo#lives_in>`。
|
||||
|
||||
从RDF的角度来看,URL `<http://my-company.com/namespace>` 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆,本节中的示例使用不可解析的URI,如`urn:example:within`。幸运的是,您只需在文件顶部指定一个前缀,然后忘记它。
|
||||
从RDF的角度来看,URL `<http://my-company.com/namespace>` 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆,本节中的示例使用不可解析的URI,如`urn:example:within`。幸运的是,你只需在文件顶部指定一个前缀,然后就不用再管了。
|
||||
|
||||
### SPARQL查询语言
|
||||
|
||||
SPARQL是RDF数据模型三元组存储的查询语言【43】。 (它是SPARQL协议和RDF查询语言的缩写,发音为“sparkle”。)它早于Cypher,并且由于Cypher的模式匹配是从SPARQL中借用的,所以它们看起来非常相似【37】。
|
||||
与从前从美国转移到欧洲的人相同的查询——在SPARQL中比在Cypher中更加简洁(参见[例2-9]())。
|
||||
**SPARQL**是一种用于三元组存储的面向RDF数据模型的查询语言,【43】。(它是SPARQL协议和RDF查询语言的缩写,发音为“sparkle”。)SPARQL早于Cypher,并且由于Cypher的模式匹配借鉴于SPARQL,这使得它们看起来非常相似【37】。
|
||||
|
||||
与之前相同的查询 - 查找从美国转移到欧洲的人 - 使用SPARQL比使用Cypher甚至更为简洁(参见[例2-9]())。
|
||||
|
||||
**例2-9 与示例2-4相同的查询,用SPARQL表示**
|
||||
|
||||
@ -799,46 +806,46 @@ SELECT ?personName WHERE {
|
||||
}
|
||||
```
|
||||
|
||||
结构非常相似。以下两个表达式是等价的(变量以SPARQL中的问号开头):
|
||||
结构非常相似。以下两个表达式是等价的(SPARQL中的变量以问号开头):
|
||||
|
||||
```
|
||||
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (location) # Cypher
|
||||
?person :bornIn / :within* ?location. # SPARQL
|
||||
```
|
||||
|
||||
由于RDF不区分属性和边,而只是使用两个属性,所以可以使用相同的语法来匹配属性。在下面的表达式中,变量usa被绑定到任何具有值为字符串`"United States"`的`name`属性的顶点:
|
||||
因为RDF不区分属性和边,而只是将它们作为谓语,所以可以使用相同的语法来匹配属性。在下面的表达式中,变量`usa`被绑定到任意具有值为字符串`"United States"`的`name`属性的顶点:
|
||||
|
||||
```
|
||||
(usa {name:'United States'}) # Cypher
|
||||
?usa :name "United States". # SPARQL
|
||||
```
|
||||
|
||||
SPARQL是一种很好的查询语言——即使语义网从来没有出现,它也可以成为应用程序内部使用的强大工具。
|
||||
SPARQL是一种很好的查询语言——哪怕语义网从未实现,它仍然可以成为一种应用程序内部使用的强大工具。
|
||||
|
||||
> #### 图形数据库与网络模型相比较
|
||||
>
|
||||
> 在“[文档数据库是否在重演历史?](#文档数据库是否在重演历史?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种?
|
||||
> 在“[文档数据库是否在重蹈覆辙?](#文档数据库是否在重蹈覆辙?)”中,我们讨论了CODASYL和关系模型如何竞相解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。CODASYL是否是图形数据库的第二个变种?
|
||||
>
|
||||
> 不,他们在几个重要方面有所不同:
|
||||
>
|
||||
> * 在CODASYL中,数据库有一个模式,指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中,不存在这样的限制:任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
|
||||
> * 在CODASYL中,数据库有一个模式,用于指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中,不存在这样的限制:任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
|
||||
> * 在CODASYL中,达到特定记录的唯一方法是遍历其中的一个访问路径。在图形数据库中,可以通过其唯一ID直接引用任何顶点,也可以使用索引来查找具有特定值的顶点。
|
||||
> * 在CODASYL,记录的孩子们的有序集合,所以数据库的人不得不维持排序(其中有用于存储布局的后果),并且插入新记录到数据库的应用程序不得不担心的新记录的位置在这些集合中。在图形数据库中,顶点和边不是有序的(您只能在查询时对结果进行排序)。
|
||||
> * 在CODASYL中,所有查询都是必要的,难以编写,并且很容易被架构中的变化破坏。在图形数据库中,如果需要,可以在命令式代码中编写遍历,但大多数图形数据库也支持高级声明式查询语言,如Cypher或SPARQL。
|
||||
> * 在CODASYL,记录的后续是一个有序集合,所以数据库的人不得不维持排序(这会影响存储布局),并且插入新记录到数据库的应用程序不得不担心的新记录在这些集合中的位置。在图形数据库中,顶点和边不是有序的(只能在查询时对结果进行排序)。
|
||||
> * 在CODASYL中,所有查询都是命令式的,难以编写,并且很容易因架构中的变化而受到破坏。在图形数据库中,如果需要,可以在命令式代码中编写遍历,但大多数图形数据库也支持高级声明式查询语言,如Cypher或SPARQL。
|
||||
>
|
||||
>
|
||||
|
||||
### 基础:Datalog
|
||||
|
||||
Datalog是比SPARQL或Cypher更古老的语言,在20世纪80年代被学者广泛研究【44,45,46】。它在软件工程师中是不太知名的,但是它是重要的,因为它为以后的查询语言提供了基础。
|
||||
**Datalog**是比SPARQL或Cypher更古老的语言,在20世纪80年代被学者广泛研究【44,45,46】。它在软件工程师中不太知名,但是它是重要的,因为它为以后的查询语言提供了基础。
|
||||
|
||||
在实践中,Datalog在一些数据系统中被使用:例如,它是Datomic 【40】的查询语言,而Cascalog 【47】是用于查询Hadoop中的大数据集的Datalog实现[^viii]。
|
||||
在实践中,Datalog被用于少数的数据系统中:例如,它是Datomic 【40】的查询语言,Cascalog 【47】是一种用于查询Hadoop大数据集的Datalog实现[^viii]。
|
||||
|
||||
[^viii]: Datomic和Cascalog使用Datalog的Clojure S表达式语法。 在下面的例子中使用了一个更容易阅读的Prolog语法,没有任何功能差异。
|
||||
[^viii]: Datomic和Cascalog使用Datalog的Clojure S表达式语法。在下面的例子中使用了一个更容易阅读的Prolog语法,但两者没有任何功能差异。
|
||||
|
||||
Datalog的数据模型类似于三元组模式,但有一点泛化。我们把它写成谓词(主语,宾语),而不是写三元语(主语,宾语,宾语)。[例2-10]()显示了如何在Datalog中写入我们的例子中的数据。
|
||||
Datalog的数据模型类似于三元组模式,但进行了一点泛化。把三元组写成**谓语**(**主语,宾语**),而不是写三元语(**主语,宾语,宾语**)。[例2-10]()显示了如何用Datalog写入我们的例子中的数据。
|
||||
|
||||
**例2-10 图2-5中的数据子集,表示为Datalog事实**
|
||||
**例2-10 用Datalog来表示图2-5中的数据子集**
|
||||
|
||||
```prolog
|
||||
name(namerica, 'North America').
|
||||
@ -856,9 +863,9 @@ name(lucy, 'Lucy').
|
||||
born_in(lucy, idaho).
|
||||
```
|
||||
|
||||
现在我们已经定义了数据,我们可以像之前一样编写相同的查询,如**例2-11**所示。它看起来有点不同于Cypher或SPARQL的等价物,但是不要让你失望。 Datalog是Prolog的一个子集,如果你已经学过计算机科学,你可能已经见过。
|
||||
既然已经定义了数据,我们可以像之前一样编写相同的查询,如[例2-11]()所示。它看起来有点不同于Cypher或SPARQL的等价物,但是请不要放弃它。Datalog是Prolog的一个子集,如果你学过计算机科学,你可能已经见过。
|
||||
|
||||
**例2-11 与示例2-4相同的查询,在Datalog中表示**
|
||||
**例2-11 与示例2-4相同的查询,用Datalog表示**
|
||||
|
||||
```
|
||||
within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */
|
||||
@ -876,52 +883,52 @@ migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
|
||||
|
||||
```
|
||||
|
||||
Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只需要一小步。我们定义告诉数据库有关新谓词的规则:在这里,我们定义了两个新的谓词,在`_recursive`和`migrated`内。这些谓词不是存储在数据库中的三元组,而是它们是从数据或其他规则派生而来的。规则可以引用其他规则,就像函数可以调用其他函数或者自发地调用自己一样。像这样,复杂的查询可以一次构建一小块。
|
||||
Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只进行一小步。我们定义**规则**,以将新谓语告诉数据库:在这里,我们定义了两个新的谓语,`_recursive`和`migrated`。这些谓语不是存储在数据库中的三元组中,而是它们是从数据或其他规则派生而来的。规则可以引用其他规则,就像函数可以调用其他函数或者递归地调用自己一样。像这样,复杂的查询可以一次构建其中的一小块。
|
||||
|
||||
在规则中,以大写字母开头的单词是变量,谓词匹配如Cypher和SPARQL。例如,`name(Location, Name)`与可变绑定`Location = namerica`和`Name ='North America'`的三重名称`(namerica, 'North America')`匹配。
|
||||
在规则中,以大写字母开头的单词是变量,谓语则用Cypher和SPARQL的方式一样来匹配。例如,`name(Location, Name)`通过变量绑定`Location = namerica`和`Name ='North America'`可以匹配三元组`name(namerica, 'North America')`。
|
||||
|
||||
如果系统可以在` -` 操作符的右侧找到与所有谓词相匹配的规则,则适用该规则。当规则适用时,就好像将`-`的左边添加到数据库(用变量替换它们的值)。
|
||||
要是系统可以在`:-` 操作符的右侧找到与所有谓语的一个匹配,就运用该规则。当规则运用时,就好像通过`:-`的左侧将其添加到数据库(将变量替换成它们匹配的值)。
|
||||
|
||||
因此,应用规则的一种可能的方式是:
|
||||
因此,一种可能的应用规则的方式是:
|
||||
|
||||
1. 名称`(namerica, '北美')`存在于数据库中,故规则1适用。它生成`within_recursive(namerica, '北美')`。
|
||||
2. 数据库中存在`(usa, namerica)`,并在之前的步骤中生成`(namerica, 'North America')`,所以适用规则2。它会产生`within_recursive(美国, “北美”)`。
|
||||
3. 在`(爱达荷州, 美国)`存在于数据库和上一步生成`within_recursive(美国, “北美”)`,所以规则2适用。它产生`within_recursive(爱达荷州, "北美")`。
|
||||
1. 数据库存在`name(namerica, 'North America')`,故运用规则1。它生成`within_recursive(namerica, 'North America')`。
|
||||
2. 数据库存在`within(usa, namerica)`,在上一步骤中生成`within_recursive(namerica, 'North America')`,故运用规则2。它会产生`within_recursive(usa, 'North America')`。
|
||||
3. 数据库存在`within(idaho, usa)`,在上一步生成`within_recursive(usa, 'North America')`,故运用规则2。它产生`within_recursive(idaho, 'North America')`。
|
||||
|
||||
通过重复应用规则1和2,`within_recursive`谓词可以告诉我们在我们的数据库中包含的北美(或任何其他位置名称)的所有位置。这个过程如[图2-6](img/fig2-6.png)所示。
|
||||
通过重复应用规则1和2,`within_recursive`谓语可以告诉我们在数据库中包含北美(或任何其他位置名称)的所有位置。这个过程如[图2-6](img/fig2-6.png)所示。
|
||||
|
||||
![](img/fig2-6.png)
|
||||
|
||||
**图2-6 使用示例2-11中的Datalog规则确定爱达荷州在北美。**
|
||||
**图2-6 使用示例2-11中的Datalog规则来确定爱达荷州在北美。**
|
||||
|
||||
现在规则3可以找到出生在某个地方BornIn的人,并住在某个地方LivingIn。通过查询`BornIn ='United States'`和`LivingIn ='Europe'`,并将此人作为变量`Who`,我们要求Datalog系统找出变量`Who`可以出现哪些值。因此,最后我们得到了与早先的Cypher和SPARQL查询相同的答案。
|
||||
现在规则3可以找到出生在某个地方`BornIn`的人,并住在某个地方`LivingIn`。通过查询`BornIn ='United States'`和`LivingIn ='Europe'`,并将此人作为变量`Who`,让Datalog系统找出变量`Who`会出现哪些值。因此,最后得到了与早先的Cypher和SPARQL查询相同的答案。
|
||||
|
||||
Datalog方法需要对本章讨论的其他查询语言采取不同的思维方式,但这是一种非常强大的方法,因为规则可以在不同的查询中进行组合和重用。简单的一次性查询不太方便,但是如果数据很复杂,它可以更好地处理。
|
||||
相对于本章讨论的其他查询语言,我们需要采取不同的思维方式来思考Datalog方法,但这是一种非常强大的方法,因为规则可以在不同的查询中进行组合和重用。虽然对于简单的一次性查询,显得不太方便,但是它可以更好地处理数据很复杂的情况。
|
||||
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
数据模型是一个巨大的课题,在本章中,我们快速浏览了各种不同的模型。我们没有足够的空间来详细介绍每个模型的细节,但是希望这个概述足以激起您的兴趣,以更多地了解最适合您的应用需求的模型。
|
||||
数据模型是一个巨大的课题,在本章中,我们快速浏览了各种不同的模型。我们没有足够的空间来详细介绍每个模型的细节,但是希望这个概述足以激起你的兴趣,以更多地了解最适合你的应用需求的模型。
|
||||
|
||||
在历史上,数据开始被表示为一棵大树(层次数据模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不适合在关系模型中使用。新的非关系型“NoSQL”数据存储在两个主要方向上有分歧:
|
||||
在历史上,数据最开始被表示为一棵大树(层次数据模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:
|
||||
|
||||
1. 文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常罕见。
|
||||
2. 图形数据库用于相反的场景: 任何东西都可能和任何东西相关联。
|
||||
1. **文档数据库**的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少。
|
||||
2. **图形数据库**用于相反的场景:任意事物都可能与任何事物相关联。
|
||||
|
||||
所有这三种模型(文档,关系和图形)今天都被广泛使用,并且在各自的领域都是很好用的。一个模型可以用另一个模型来模拟 —— 例如,图数据可以在关系数据库中表示 —— 但结果往往是尴尬的。这就是为什么我们有着用于不同目的的不同系统,而不是一个单一的万能解决方案。
|
||||
这三种模型(文档,关系和图形)在今天都被广泛使用,并且在各自的领域都发挥很好。一个模型可以用另一个模型来模拟 — 例如,图数据可以在关系数据库中表示 — 但结果往往是糟糕的。这就是为什么我们有着针对不同目的的不同系统,而不是一个单一的万能解决方案。
|
||||
|
||||
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制实施一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍假定数据具有一定的结构:这只是模式是明确的(强制写入)还是隐含的(在读取时处理)的问题。
|
||||
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍会假定数据具有一定的结构;这只是模式是明确的(写入时强制)还是隐含的(读取时处理)的问题。
|
||||
|
||||
每个数据模型都带有自己的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和 XSL/XPath,它们不是数据库查询语言,而包含有趣的相似之处。
|
||||
每个数据模型都具有各自的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和XSL/XPath,它们不是数据库查询语言,而包含有趣的相似之处。
|
||||
|
||||
虽然我们已经覆盖了很多地方,但仍然有许多数据模型没有提到。举几个简单的例子:
|
||||
虽然我们已经覆盖了很多层面,但仍然有许多数据模型没有提到。举几个简单的例子:
|
||||
|
||||
* 研究人员使用基因组数据通常需要执行序列相似性搜索,这意味着需要一个很长的字符串(代表一个DNA分子),并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻止硬件成本从失控中解脱出来【49】。
|
||||
* 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索索引。
|
||||
* 使用基因组数据的研究人员通常需要执行**序列相似性搜索**,这意味着需要一个很长的字符串(代表一个DNA分子),并在一个拥有类似但不完全相同的字符串的大型数据库中寻找匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻住硬件成本的失控【49】。
|
||||
* **全文搜索**可以说是一种经常与数据库一起使用的数据模型。信息检索是一个很大的专业课题,我们不会在本书中详细介绍,但是我们将在第三章和第三章中介绍搜索索引。
|
||||
|
||||
在[下一章](ch3.md)中,我们将讨论在实现本章描述的数据模型时会遇到的一些权衡。
|
||||
让我们暂时将其放在一边。在[下一章](ch3.md)中,我们将讨论在**实现**本章描述的数据模型时会遇到的一些权衡。
|
||||
|
||||
|
||||
|
||||
|
49
ch3.md
49
ch3.md
@ -205,7 +205,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是大小分层压实。 LevelDB和RocksDB使用平坦压缩(LevelDB因此得名),HBase使用大小分层,Cassandra同时支持【16】。在规模级别的调整中,更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在水平压实中,关键范围被拆分成更小的SSTables,而较旧的数据被移动到单独的“水平”,这使得压缩能够更加递增地进行,并且使用更少的磁盘空间。
|
||||
|
||||
即使有许多微妙的东西,LSM树的基本思想 - 保存一系列在后台合并的SSTables - 简单而有效。即使数据集比可用内存大得多,它仍能继续正常工作。由于数据按排序顺序存储,因此可以高效地执行范围查询(扫描所有高于某些最小值和最高值的所有键),并且因为磁盘写入是连续的,所以LSM-tree可以支持非常高的写入吞吐量。
|
||||
即使有许多微妙的东西,LSM树的基本思想 —— 保存一系列在后台合并的SSTables —— 简单而有效。即使数据集比可用内存大得多,它仍能继续正常工作。由于数据按排序顺序存储,因此可以高效地执行范围查询(扫描所有高于某些最小值和最高值的所有键),并且因为磁盘写入是连续的,所以LSM树可以支持非常高的写入吞吐量。
|
||||
|
||||
|
||||
|
||||
@ -215,7 +215,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
像SSTables一样,B树保持按键排序的键值对,这允许高效的键值查找和范围查询。但这就是相似之处的结尾:B树有着非常不同的设计理念。
|
||||
|
||||
我们前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B树将数据库分解成固定大小的块或页面,传统上大小为4 KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中。
|
||||
我们前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B树将数据库分解成固定大小的块或页面,传统上大小为4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中。
|
||||
|
||||
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。
|
||||
|
||||
@ -225,11 +225,11 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
一个页面会被指定为B树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键,引用之间的键,指明了引用子页面的键范围。
|
||||
|
||||
在[图3-6](img/fig3-6.png)的例子中,我们正在寻找关键字251,所以我们知道我们需要遵循边界200和300之间的页面引用。这将我们带到一个类似的页面,进一步打破了200-300到子范围。
|
||||
在[图3-6](img/fig3-6.png)的例子中,我们正在寻找关键字 251 ,所以我们知道我们需要遵循边界 200 和 300 之间的页面引用。这将我们带到一个类似的页面,进一步打破了200 - 300到子范围。
|
||||
|
||||
最后,我们可以看到包含单个键(叶页)的页面,该页面包含每个键的内联值,或者包含对可以找到值的页面的引用。
|
||||
|
||||
在B树的一个页面中对子页面的引用的数量称为分支因子。例如,在[图3-6](img/fig3-6.png)中,分支因子是6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。
|
||||
在B树的一个页面中对子页面的引用的数量称为分支因子。例如,在[图3-6](img/fig3-6.png)中,分支因子是 6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。
|
||||
|
||||
如果要更新B树中现有键的值,则搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘(对该页的任何引用保持有效) 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区,如[图3-7](img/fig3-7.png)所示[^ii]。
|
||||
|
||||
@ -239,7 +239,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
**图3-7 通过分割页面来生长B树**
|
||||
|
||||
该算法确保树保持平衡:具有 n 个键的B树总是具有$O(log n)$的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵循许多页面引用来找到你正在查找的页面。 (分支因子为500的4KB页面的四级树可以存储多达256 TB。)
|
||||
该算法确保树保持平衡:具有 n 个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵追踪多页面引用来找到你正在查找的页面。 (分支因子为 500 的 4KB 页面的四级树可以存储多达 256TB 。)
|
||||
|
||||
#### 让B树更可靠
|
||||
|
||||
@ -249,9 +249,9 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
|
||||
|
||||
为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:**预写式日志(write-ahead-log)**(WAL,也称为重做日志)。这是一个只能追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态【5,20】。
|
||||
为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:**预写式日志(WAL, write-ahead-log)**(也称为**重做日志(redo log)**)。这是一个仅追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态【5,20】。
|
||||
|
||||
更新页面的一个额外的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制 - 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器(latches)**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。
|
||||
更新页面的一个额外的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器(latches)**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。
|
||||
|
||||
#### B树优化
|
||||
|
||||
@ -259,13 +259,13 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
* 一些数据库(如LMDB)使用写时复制方案【21】,而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置,并且树中的父页面的新版本被创建,指向新的位置。这种方法对于并发控制也很有用,我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
|
||||
* 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B-树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
|
||||
* B树的变体如分形树[22]借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
|
||||
* B树的变体如分形树【22】借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
|
||||
|
||||
### 比较B树和LSM树
|
||||
|
||||
尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,LSM树通常写速度更快,而B树被认为读取速度更快【23】。 LSM树上的读取通常比较慢,因为他们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
|
||||
尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,通常LSM树的写入速度更快,而B树的读取速度更快【23】。 LSM树上的读取通常比较慢,因为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
|
||||
|
||||
然而,基准通常对工作量的细节不确定和敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。 在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
|
||||
@ -273,11 +273,11 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
B树索引必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次,以免在电源故障的情况下导致页面部分更新【24,25】。
|
||||
|
||||
由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 - 在数据库的生命周期中写入数据库导致对磁盘的多次写入 - 被称为**写放大(Write amplification)**。固态硬盘是特别值得关注的,固态硬盘在磨损之前只能覆盖一段时间。
|
||||
由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)**。需要特别关注的是固态硬盘,固态硬盘在磨损之前只能覆写一段时间。
|
||||
|
||||
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写入放大具有直接的性能成本:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
|
||||
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
|
||||
|
||||
而且,LSM树通常能够比B-树支持更高的写入吞吐量,部分原因是它们有时具有较低的写入放大(尽管这取决于存储引擎配置和工作负载),部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆盖树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要,顺序写入比随机写入快得多。
|
||||
而且,LSM树通常能够比B树支持更高的写入吞吐量,部分原因是它们有时具有较低的写放大(尽管这取决于存储引擎配置和工作负载),部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆盖树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要,顺序写入比随机写入快得多。
|
||||
|
||||
LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的文件。 B树存储引擎会由于分割而留下一些未使用的磁盘空间:当页面被拆分或某行不能放入现有页面时,页面中的某些空间仍未被使用。由于LSM树不是面向页面的,并且定期重写SSTables以去除碎片,所以它们具有较低的存储开销,特别是当使用平坦压缩时【27】。
|
||||
|
||||
@ -287,7 +287,7 @@ LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的
|
||||
|
||||
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性【28】。
|
||||
|
||||
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新memtable到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
|
||||
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
|
||||
|
||||
如果写入吞吐量很高,并且压缩没有仔细配置,压缩跟不上写入速率。在这种情况下,磁盘上未合并段的数量不断增加,直到磁盘空间用完,读取速度也会减慢,因为它们需要检查更多段文件。通常情况下,即使压缩无法跟上,基于SSTable的存储引擎也不会限制传入写入的速率,所以您需要进行明确的监控来检测这种情况【29,30】。
|
||||
|
||||
@ -361,7 +361,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
最近的研究表明,内存数据库体系结构可以扩展到支持比可用内存更大的数据集,而不必重新采用以磁盘为中心的体系结构【45】。所谓的**反缓存(anti-caching)**方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘,并在将来再次访问时将其重新加载到内存中。这与操作系统对虚拟内存和交换文件的操作类似,但数据库可以比操作系统更有效地管理内存,因为它可以按单个记录的粒度工作,而不是整个内存页面。尽管如此,这种方法仍然需要索引能完全放入内存中(就像本章开头的Bitcask例子)。
|
||||
|
||||
如果非易失性存储器(NVM)技术得到更广泛的应用,可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域,值得关注。
|
||||
如果**非易失性存储器(NVM)**技术得到更广泛的应用,可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域,值得关注。
|
||||
|
||||
|
||||
|
||||
@ -390,7 +390,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
| 处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 |
|
||||
| 数据集尺寸 | GB ~ TB | TB ~ PB |
|
||||
|
||||
起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此,在二十世纪八十年代末和九十年代初期,公司有停止使用OLTP系统进行分析的趋势,而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库**。
|
||||
起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此,在二十世纪八十年代末和九十年代初期,公司有停止使用OLTP系统进行分析的趋势,而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库(data warehouse)**。
|
||||
|
||||
### 数据仓库
|
||||
|
||||
@ -404,7 +404,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
**图3-8 ETL至数据仓库的简化提纲**
|
||||
|
||||
几乎所有的大型企业都有数据仓库,但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统,大多数小公司只有少量的数据——可以在传统的SQL数据库中查询,甚至可以在电子表格中分析。在一家大公司里,要做一些在一家小公司很简单的事情,需要很多繁重的工作。
|
||||
几乎所有的大型企业都有数据仓库,但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统,大多数小公司只有少量的数据 —— 可以在传统的SQL数据库中查询,甚至可以在电子表格中分析。在一家大公司里,要做一些在一家小公司很简单的事情,需要很多繁重的工作。
|
||||
|
||||
使用单独的数据仓库,而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明,本章前半部分讨论的索引算法对于OLTP来说工作得很好,但对于回答分析查询并不是很好。在本章的其余部分中,我们将看看为分析而优化的存储引擎。
|
||||
|
||||
@ -495,7 +495,7 @@ GROUP BY
|
||||
|
||||
**图3-11 压缩位图索引存储布局**
|
||||
|
||||
通常情况下,一列中不同值的数量与行数相比较小(例如,零售商可能有数十亿的销售交易,但只有100,000个不同的产品)。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图:每个不同值的一个位图,每行一位。如果该行具有该值,则该位为1,否则为0。
|
||||
通常情况下,一列中不同值的数量与行数相比较小(例如,零售商可能有数十亿的销售交易,但只有100,000个不同的产品)。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图:每个不同值的一个位图,每行一位。如果该行具有该值,则该位为 1 ,否则为 0 。
|
||||
|
||||
如果 n 非常小(例如,国家/地区列可能有大约200个不同的值),则这些位图可以每行存储一位。但是,如果n更大,大部分位图中将会有很多的零(我们说它们是稀疏的)。在这种情况下,位图可以另外进行游程编码,如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。
|
||||
|
||||
@ -554,7 +554,7 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
使用B树的更新就地方法对于压缩的列是不可能的。如果你想在排序表的中间插入一行,你很可能不得不重写所有的列文件。由于行由列中的位置标识,因此插入必须始终更新所有列。
|
||||
|
||||
幸运的是,本章前面已经看到了一个很好的解决方案:LSM树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入磁盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的[62]。
|
||||
幸运的是,本章前面已经看到了一个很好的解决方案:LSM树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入磁盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的【62】。
|
||||
|
||||
查询需要检查磁盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器隐藏了用户的这个区别。从分析师的角度来看,通过插入,更新或删除操作进行修改的数据会立即反映在后续查询中。
|
||||
|
||||
@ -588,15 +588,20 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
在本章中,我们试图深入了解数据库如何处理存储和检索。将数据存储在数据库中会发生什么,以及稍后再次查询数据时数据库会做什么?
|
||||
|
||||
在高层次上,我们看到存储引擎分为两大类:优化事务处理(OLTP)和优化分析(OLAP)的类别。这些用例的访问模式之间有很大的区别:
|
||||
在高层次上,我们看到存储引擎分为两大类:优化**事务处理(OLTP)**和**优化分析(OLAP)**的类别。这些用例的访问模式之间有很大的区别:
|
||||
|
||||
* OLTP系统通常面向用户,这意味着他们可能会看到大量的请求。为了处理负载,应用程序通常只触及每个查询中的少量记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是这里的瓶颈。
|
||||
* 数据仓库和类似的分析系统不太知名,因为它们主要由业务分析人员使用,而不是由最终用户使用。它们处理比OLTP系统少得多的查询量,但是每个查询通常要求很高,需要在短时间内扫描数百万条记录。磁盘带宽(不是查找时间)往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
|
||||
|
||||
在OLTP方面,我们看到了来自两大主流学派的存储引擎:
|
||||
|
||||
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个组。
|
||||
* 就地更新学派,将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子,被用在所有主要的关系数据库中,还有许多非关系数据库。
|
||||
***日志结构学派***
|
||||
|
||||
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个组。
|
||||
|
||||
***就地更新学派***
|
||||
|
||||
将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子,被用在所有主要的关系数据库中,还有许多非关系数据库。
|
||||
|
||||
日志结构的存储引擎是相对较新的发展。他们的主要想法是,他们系统地将随机访问写入顺序写入磁盘,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。在完成OLTP方面,我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。
|
||||
|
||||
|
2
ch5.md
2
ch5.md
@ -199,7 +199,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
|
||||
|
||||
因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
|
||||
|
||||
### 读已之写
|
||||
### 读己之写
|
||||
|
||||
许多应用程序让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。
|
||||
|
||||
|
2
ch6.md
2
ch6.md
@ -406,3 +406,5 @@ Couchbase不会自动重新平衡,这简化了设计。通常情况下,它
|
||||
| :--------------------: | :-----------------------------: | :--------------------: |
|
||||
| [第五章:复制](ch5.md) | [设计数据密集型应用](README.md) | [第七章:事务](ch7.md) |
|
||||
|
||||
ou
|
||||
|
||||
|
20
ch7.md
20
ch7.md
@ -475,17 +475,17 @@ UPDATE wiki_pages SET content = '新内容'
|
||||
|
||||
首先,想象一下这个例子:你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作【40,41】。
|
||||
|
||||
现在想象一下,Alice和Bob是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮关闭电话。[图7-8](img/fig7-8.png)说明了接下来的事情。
|
||||
现在想象一下,Alice和Bob是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。[图7-8](img/fig7-8.png)说明了接下来的事情。
|
||||
|
||||
![](img/fig7-8.png)
|
||||
|
||||
**图7-8 写入偏差导致应用程序错误的示例**
|
||||
|
||||
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地翘班。由于数据库使用快照隔离,两次检查都返回2,所以两个事务都进入下一个阶段。爱丽丝更新自己的记录翘班了,而鲍勃也干了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
|
||||
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 ,所以两个事务都进入下一个阶段。Alice更新自己的记录休班了,而Bob也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
|
||||
|
||||
#### 写偏差的特征
|
||||
|
||||
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**,因为这两个事务正在更新两个不同的对象(Alice和Bob各自的待命记录)。在这里发生的冲突并不是那么明显,但是这显然是一个竞争条件:如果两个事务一个接一个地运行,那么第二个医生就不能翘班了。异常行为只有在事务并发进行时才有可能。
|
||||
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**,因为这两个事务正在更新两个不同的对象(Alice和Bob各自的待命记录)。在这里发生的冲突并不是那么明显,但是这显然是一个竞争条件:如果两个事务一个接一个地运行,那么第二个医生就不能歇班了。异常行为只有在事务并发进行时才有可能。
|
||||
|
||||
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时机)。
|
||||
|
||||
@ -543,7 +543,7 @@ COMMIT;
|
||||
|
||||
***多人游戏***
|
||||
|
||||
在例7-1中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照您正在执行的规则类型,也许可以使用唯一约束,否则您很容易发生写入偏差。
|
||||
在[例7-1]()中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照您正在执行的规则类型,也许可以使用唯一约束,否则您很容易发生写入偏差。
|
||||
|
||||
***抢注用户名***
|
||||
|
||||
@ -563,11 +563,11 @@ COMMIT;
|
||||
|
||||
3. 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。
|
||||
|
||||
这个写入的效果改变了步骤2中的前决条件。换句话说,如果在提交写入后,重复执行一次步骤1的SELECT查询,将会得到不同的结果。因为写入改变符合搜索条件的行集(现在少了一个医生值班,那时候的会议室现在已经被预订了,棋盘上的这个位置已经被占据了,用户名已经被抢注,账户余额不够了)。
|
||||
这个写入的效果改变了步骤2 中的先决条件。换句话说,如果在提交写入后,重复执行一次步骤1 的SELECT查询,将会得到不同的结果。因为写入改变符合搜索条件的行集(现在少了一个医生值班,那时候的会议室现在已经被预订了,棋盘上的这个位置已经被占据了,用户名已经被抢注,账户余额不够了)。
|
||||
|
||||
这些步骤可能以不同的顺序发生。例如可以首先进行写入,然后进行SELECT查询,最后根据查询结果决定是放弃还是提交。
|
||||
|
||||
在医生值班的例子中,在步骤3中修改的行,是步骤1中返回的行之一,所以我们可以通过锁定步骤1中的行(`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否**不存在**某些满足条件的行,写入会**添加**一个匹配相同条件的行。如果步骤1中的查询没有返回任何行,则`SELECT FOR UPDATE`锁不了任何东西。
|
||||
在医生值班的例子中,在步骤3中修改的行,是步骤1中返回的行之一,所以我们可以通过锁定步骤1 中的行(`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否**不存在**某些满足条件的行,写入会**添加**一个匹配相同条件的行。如果步骤1中的查询没有返回任何行,则`SELECT FOR UPDATE`锁不了任何东西。
|
||||
|
||||
这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为**幻读**【3】。快照隔离避免了只读查询中幻读,但是在像我们讨论的例子那样的读写事务中,幻影会导致特别棘手的写歪斜情况。
|
||||
|
||||
@ -589,11 +589,11 @@ COMMIT;
|
||||
|
||||
- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读”的含义天差地别)。
|
||||
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,您可能并不知道并发发生的所有事情。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的——只有在倒霉的时机下才会出现问题。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时机下才会出现问题。
|
||||
|
||||
这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可序列化(serializable)**的隔离级别!
|
||||
|
||||
**可序列化(Serializability)**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确——换句话说,数据库可以防止**所有**可能的竞争条件。
|
||||
**可序列化(Serializability)**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。
|
||||
|
||||
但如果可序列化隔离级别比弱隔离级别的烂摊子要好得多,那为什么没有人见人爱?为了回答这个问题,我们需要看看实现可序列化的选项,以及它们如何执行。目前大多数提供可序列化的数据库都使用了三种技术之一,本章的剩余部分将会介绍这些技术。
|
||||
|
||||
@ -780,9 +780,9 @@ WHERE room_id = 123 AND
|
||||
|
||||
先前讨论了快照隔离中的写入偏差(参阅“[写入偏差和幻像](#写入偏差与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
|
||||
换句话说,事务基于一个**前提(premise)**采取行动(事务开始时候的事实,例如:“目前有两名医生正在通话”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
换句话说,事务基于一个**前提(premise)**采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
|
||||
数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:
|
||||
|
||||
|
2
ch8.md
2
ch8.md
@ -18,7 +18,7 @@
|
||||
|
||||
最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了**副本故障转移**(“[处理节点中断](#ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch6.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。
|
||||
|
||||
但是,尽管我们已经谈了很多错误,但最后几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事)
|
||||
但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事)
|
||||
|
||||
[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(参见“[拜占庭故障](#拜占庭故障)”)。
|
||||
|
||||
|
14
ch9.md
14
ch9.md
@ -35,7 +35,7 @@
|
||||
|
||||
大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的复本最终会收敛到相同的值【2】。
|
||||
|
||||
然而,这是一个非常弱的保证——它并没有说什么什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
|
||||
对于应用程序开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。如果将一个值赋给一个变量,然后很快地再次读取,你不会认为可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。
|
||||
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
在[图9-4]()中,除了读写之外,还增加了第三种类型的操作:
|
||||
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠vold$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。
|
||||
|
||||
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
|
||||
|
||||
@ -216,7 +216,7 @@
|
||||
|
||||
***共识算法(线性一致)***
|
||||
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,协调算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
|
||||
***多主复制(非线性一致)***
|
||||
|
||||
@ -252,7 +252,7 @@
|
||||
|
||||
一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。
|
||||
|
||||
我们已经在[第五章中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
|
||||
我们已经在[第五章](ch5.md)中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
|
||||
|
||||
![](img/fig9-7.png)
|
||||
|
||||
@ -358,7 +358,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。
|
||||
|
||||
并发意味着时间线会分岔然后合并——在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖——操作的偏序。
|
||||
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
|
||||
|
||||
如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)**会在这些并发创建的提交相融合时创建。
|
||||
|
||||
@ -450,7 +450,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。
|
||||
|
||||
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导者与锁定]](ch8.md#领导者与锁定)”中提到过这个问题。)
|
||||
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导者与锁定](ch8.md#领导者与锁定)”中提到过这个问题。)
|
||||
|
||||
乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。
|
||||
|
||||
@ -538,7 +538,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行**自增并返回**操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号连续发送消息。
|
||||
|
||||
请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同 ——事实上,这是全序广播和时间戳排序间的关键区别。
|
||||
请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同 —— 事实上,这是全序广播和时间戳排序间的关键区别。
|
||||
|
||||
实现一个带有原子性**自增并返回**操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行深入过足够深入的思考,你不可避免地会得出一个共识算法。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user