博文原文地址:http://blog.jobbole.com/100349html
一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,并且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但不多有文章讲解数据库是如何工做的。你能够本身谷歌/百度一下『关系型数据库原理』,看看结果多么的稀少【译者注:百度为您找到相关结果约1,850,000个…】 ,并且找到的那些文章都很短。如今若是你查找最近时髦的技术(大数据、NoSQL或JavaScript),你能找到更多深刻探讨它们如何工做的文章。java
难道关系型数据库已经太古老太无趣,除了大学教材、研究文献和书籍之外,没人愿意讲了吗?mysql
做为一个开发人员,我不喜欢用我不明白的东西。并且,数据库已经使用了40年之久,必定有理由的。多年以来,我花了成百上千个小时来真正领会这些我天天都在用的、古怪的黑盒子。关系型数据库很是有趣,由于它们是基于实用并且可复用的概念。若是你对了解一个数据库感兴趣,可是从未有时间或意愿来刻苦钻研这个内容普遍的课题,你应该喜欢这篇文章。算法
虽然本文标题很明确,但个人目的并非讲如何使用数据库。所以,你应该已经掌握怎么写一个简单的 join query(联接查询)和CRUD操做(建立读取更新删除),不然你可能没法理解本文。这是惟一须要你了解的,其余的由我来说解。sql
我会从一些计算机科学方面的知识谈起,好比时间复杂度。我知道有些人讨厌这个概念,可是没有它你就不能理解数据库内部的巧妙之处。因为这是个很大的话题,我将集中探讨我认为必要的内容:数据库处理SQL查询的方式。我仅仅介绍数据库背后的基本概念,以便在读完本文后你会对底层到底发生了什么有个很好的了解。数据库
【译者注:关于时间复杂度。计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。若是不了解这个概念建议先看看维基或百度百科,对于理解文章下面的内容颇有帮助】apache
因为本文是个长篇技术文章,涉及到不少算法和数据结构知识,你尽能够慢慢读。有些概念比较难懂,你能够跳过,不影响理解总体内容。编程
这篇文章大约分为3个部分:api
好久好久之前(在一个遥远而又遥远的星系……),开发者必须确切地知道他们的代码须要多少次运算。他们把算法和数据结构牢记于心,由于他们的计算机运行缓慢,没法承受对CPU和内存的浪费。缓存
在这一部分,我将提醒你们一些这类的概念,由于它们对理解数据库相当重要。我还会介绍数据库索引的概念。
现今不少开发者不关心时间复杂度……他们是对的。
可是当你应对大量的数据(我说的可不仅是成千上万哈)或者你要争取毫秒级操做,那么理解这个概念就很关键了。并且你猜怎么着,数据库要同时处理这两种情景!我不会占用你太长时间,只要你能明白这一点就够了。这个概念在下文会帮助咱们理解什么是基于成本的优化。
时间复杂度用来检验某个算法处理必定量的数据要花多长时间。为了描述这个复杂度,计算机科学家使用数学上的『简明解释算法中的大O符号』。这个表示法用一个函数来描述算法处理给定的数据须要多少次运算。
好比,当我说『这个算法是适用 O(某函数())』,个人意思是对于某些数据,这个算法须要 某函数(数据量) 次运算来完成。
重要的不是数据量,而是当数据量增长时运算如何增长。时间复杂度不会给出确切的运算次数,可是给出的是一种理念。
图中能够看到不一样类型的复杂度的演变过程,我用了对数尺来建这个图。具体点儿说,数据量以很快的速度从1条增加到10亿条。咱们可获得以下结论:
数据量低时,O(1) 和 O(n^2)的区别能够忽略不计。好比,你有个算法要处理2000条元素。
O(1) 和 O(n^2) 的区别彷佛很大(4百万),但你最多损失 2 毫秒,只是一眨眼的功夫。确实,当今处理器每秒可处理上亿次的运算。这就是为何性能和优化在不少IT项目中不是问题。
我说过,面临海量数据的时候,了解这个概念依然很重要。若是这一次算法须要处理 1,000,000 条元素(这对数据库来讲也不算大)。
我没有具体算过,但我要说,用O(n^2) 算法的话你有时间喝杯咖啡(甚至再续一杯!)。若是在数据量后面加个0,那你就能够去睡大觉了。
为了让你能明白
注:在接下来的部分,咱们将会研究这些算法和数据结构。
有多种类型的时间复杂度
时间复杂度常常处于最差状况场景。
这里我只探讨时间复杂度,但复杂度还包括:
固然还有比 n^2 更糟糕的复杂度,好比:
注:我并无给出『大O表示法』的真正定义,只是利用这个概念。能够看看维基百科上的这篇文章。
当你要对一个集合排序时你怎么作?什么?调用 sort() 函数……好吧,算你对了……可是对于数据库,你须要理解这个 sort() 函数的工做原理。
优秀的排序算法有好几个,我侧重于最重要的一种:合并排序。你如今可能还不了解数据排序有什么用,但看完查询优化部分后你就会知道了。再者,合并排序有助于咱们之后理解数据库常见的联接操做,即合并联接 。
与不少有用的算法相似,合并排序基于这样一个技巧:将 2 个大小为 N/2 的已排序序列合并为一个 N 元素已排序序列仅须要 N 次操做。这个方法叫作合并。
咱们用个简单的例子来看看这是什么意思:
经过此图你能够看到,在 2 个 4元素序列里你只须要迭代一次,就能构建最终的8元素已排序序列,由于两个4元素序列已经排好序了:
这个方法之因此有效,是由于两个4元素序列都已经排好序,你不须要再『回到』序列中查找比较。
【译者注:合并排序详细原理,其中一个动图(原图较长,我作了删减)清晰的演示了上述合并排序的过程,而原文的叙述彷佛没有这么清晰,不动戳大。】
既然咱们明白了这个技巧,下面就是个人合并排序伪代码。
C
1 2 3 4 5 6 7 8 9 10 11 12 13 |
array mergeSort(array a) if(length(a)==1) return a[0]; end if
//recursive calls [left_array right_array] := split_into_2_equally_sized_arrays(a); array new_left_array := mergeSort(left_array); array new_right_array := mergeSort(right_array);
//merging the 2 small ordered arrays into a big one array result := merge(new_left_array,new_right_array); return result; |
合并排序是把问题拆分为小问题,经过解决小问题来解决最初的问题(注:这种算法叫分治法,即『分而治之、各个击破』)。若是你不懂,不用担忧,我第一次接触时也不懂。若是能帮助你理解的话,我认为这个算法是个两步算法:
在拆分阶段过程当中,使用3个步骤将序列分为一元序列。步骤数量的值是 log(N) (由于 N=8, log(N)=3)。【译者注:底数为2,下文有说明】
我怎么知道这个的?
我是天才!一句话:数学。道理是每一步都把原序列的长度除以2,步骤数就是你能把原序列长度除以2的次数。这正好是对数的定义(在底数为2时)。
在排序阶段,你从一元序列开始。在每个步骤中,你应用屡次合并操做,成本一共是 N=8 次运算。
由于有 log(N) 个步骤,总体成本是 N*log(N) 次运算。
【译者注:这个完整的动图演示了拆分和排序的全过程,不动戳大。】
为何这个算法如此强大?
由于:
注:这种算法叫『原地算法』(in-place algorithm)
注:这种算法叫『外部排序』(external sorting)。
好比,分布式合并排序是Hadoop(那个著名的大数据框架)的关键组件之一。
这个排序算法在大多数(若是不是所有的话)数据库中使用,可是它并非惟一算法。若是你想多了解一些,你能够看看 这篇论文,探讨的是数据库中经常使用排序算法的优点和劣势。
既然咱们已经了解了时间复杂度和排序背后的理念,我必需要向你介绍3种数据结构了。这个很重要,由于它们是现代数据库的支柱。我还会介绍数据库索引的概念。
二维阵列是最简单的数据结构。一个表能够看做是个阵列,好比:
这个二维阵列是带有行与列的表:
虽然用这个方法保存和视觉化数据很棒,可是当你要查找特定的值它就很糟糕了。 举个例子,若是你要找到全部在 UK 工做的人,你必须查看每一行以判断该行是否属于 UK 。这会形成 N 次运算的成本(N 等于行数),还不赖嘛,可是有没有更快的方法呢?这时候树就能够登场了(或开始起做用了)。
二叉查找树是带有特殊属性的二叉树,每一个节点的关键字必须:
【译者注:binary search tree,二叉查找树/二叉搜索树,或称 Binary Sort Tree 二叉排序树。见百度百科 】
这个树有 N=15 个元素。比方说我要找208:
如今比方说我要找40
最后,两次查询的成本就是树内部的层数。若是你仔细阅读了合并排序的部分,你就应该明白一共有 log(N)层。因此这个查询的成本是 log(N),不错啊!
上文说的很抽象,咱们回来看看咱们的问题。此次不用傻傻的数字了,想象一下前表中表明某人的国家的字符串。假设你有个树包含表中的列『country』:
此次搜索只需 log(N) 次运算,而若是你直接使用阵列则须要 N 次运算。你刚刚想象的就是一个数据库索引。
查找一个特定值这个树挺好用,可是当你须要查找两个值之间的多个元素时,就会有大麻烦了。你的成本将是 O(N),由于你必须查找树的每个节点,以判断它是否处于那 2 个值之间(例如,对树使用中序遍历)。并且这个操做不是磁盘I/O有利的,由于你必须读取整个树。咱们须要找到高效的范围查询方法。为了解决这个问题,现代数据库使用了一种修订版的树,叫作B+树。在一个B+树里:
你能够看到,节点更多了(多了两倍)。确实,你有了额外的节点,它们就是帮助你找到正确节点的『决策节点』(正确节点保存着相关表中行的位置)。可是搜索复杂度仍是在 O(log(N))(只多了一层)。一个重要的不一样点是,最底层的节点是跟后续节点相链接的。
用这个 B+树,假设你要找40到100间的值:
比方说你找到了 M 个后续节点,树总共有 N 个节点。对指定节点的搜索成本是 log(N),跟上一个树相同。可是当你找到这个节点,你得经过后续节点的链接获得 M 个后续节点,这须要 M 次运算。那么此次搜索只消耗了 M+log(N) 次运算,区别于上一个树所用的 N 次运算。此外,你不须要读取整个树(仅须要读 M+log(N) 个节点),这意味着更少的磁盘访问。若是 M 很小(好比 200 行)而且 N 很大(1,000,000),那结果就是天壤之别了。
然而还有新的问题(又来了!)。若是你在数据库中增长或删除一行(从而在相关的 B+树索引里):
换句话说,B+树须要自我整理和自我平衡。谢天谢地,咱们有智能删除和插入。可是这样也带来了成本:在B+树中,插入和删除操做是 O(log(N)) 复杂度。因此有些人听到过使用太多索引不是个好主意这类说法。没错,你减慢了快速插入/更新/删除表中的一个行的操做,由于数据库须要以代价高昂的每索引 O(log(N)) 运算来更新表的索引。再者,增长索引意味着给事务管理器带来更多的工做负荷(在本文结尾咱们会探讨这个管理器)。
想了解更多细节,你能够看看 Wikipedia 上这篇关于B+树的文章。若是你想要数据库中实现B+树的例子,看看MySQL核心开发人员写的这篇文章 和 这篇文章。两篇文章都致力于探讨 innoDB(MySQL引擎)如何处理索引。
咱们最后一个重要的数据结构是哈希表。当你想快速查找值时,哈希表是很是有用的。并且,理解哈希表会帮助咱们接下来理解一个数据库常见的联接操做,叫作『哈希联接』。这个数据结构也被数据库用来保存一些内部的东西(好比锁表或者缓冲池,咱们在下文会研究这两个概念)。
哈希表这种数据结构能够用关键字来快速找到一个元素。为了构建一个哈希表,你须要定义:
咱们来看一个形象化的例子:
这个哈希表有10个哈希桶。由于我懒,我只给出5个桶,可是我知道你很聪明,因此我让你想象其它的5个桶。我用的哈希函数是关键字对10取模,也就是我只保留元素关键字的最后一位,用来查找它的哈希桶:
【译者注:取模运算】
比方说你要找元素 78:
如今,比方说你要找元素 59:
你能够看到,根据你查找的值,成本并不相同。
若是我把哈希函数改成关键字对 1,000,000 取模(就是说取后6位数字),第二次搜索只消耗一次运算,由于哈希桶 00059 里面没有元素。真正的挑战是找到好的哈希函数,让哈希桶里包含很是少的元素。
在个人例子里,找到一个好的哈希函数很容易,但这是个简单的例子。当关键字是下列形式时,好的哈希函数就更难找了:
若是有了好的哈希函数,在哈希表里搜索的时间复杂度是 O(1)。
为何不用阵列呢?
嗯,你问得好。
想要更详细的信息,你能够阅读我在Java HashMap 上的文章,是关于高效哈希表实现的。你不须要了解Java就能理解文章里的概念。
咱们已经了解了数据库内部的基本组件,如今咱们须要回来看看数据库的全貌了。
数据库是一个易于访问和修改的信息集合。不过简单的一堆文件也能达到这个效果。事实上,像SQLite这样最简单的数据库也只是一堆文件而已,但SQLite是精心设计的一堆文件,由于它容许你:
数据库通常能够用以下图形来理解:
撰写这部分以前,我读过不少书/论文,它们都以本身的方式描述数据库。因此,我不会特别关注如何组织数据库或者如何命名各类进程,由于我选择了本身的方式来描述这些概念以适应本文。区别就是不一样的组件,整体思路为:数据库是由多种互相交互的组件构成的。
核心组件:
工具:
查询管理器:
数据管理器:
在本文剩余部分,我会集中探讨数据库如何经过以下进程管理SQL查询的:
客户端管理器是处理客户端通讯的。客户端能够是一个(网站)服务器或者一个最终用户或最终应用。客户端管理器经过一系列知名的API(JDBC, ODBC, OLE-DB …)提供不一样的方式来访问数据库。
客户端管理器也提供专有的数据库访问API。
当你链接到数据库时:
这部分是数据库的威力所在,在这部分里,一个写得糟糕的查询能够转换成一个快速执行的代码,代码执行的结果被送到客户端管理器。这个多步骤操做过程以下:
这里我不会过多探讨最后两步,由于它们不过重要。
看完这部分后,若是你须要更深刻的知识,我建议你阅读:
每一条SQL语句都要送到解析器来检查语法,若是你的查询有错,解析器将拒绝该查询。好比,若是你写成”SLECT …” 而不是 “SELECT …”,那就没有下文了。
但这还不算完,解析器还会检查关键字是否使用正确的顺序,好比 WHERE 写在 SELECT 以前会被拒绝。
而后,解析器要分析查询中的表和字段,使用数据库元数据来检查:
接着,解析器检查在查询中你是否有权限来读取(或写入)表。再强调一次:这些权限由DBA分配。
在解析过程当中,SQL 查询被转换为内部表示(一般是一个树)。
若是一切正常,内部表示被送到查询重写器。
在这一步,咱们已经有了查询的内部表示,重写器的目标是:
重写器按照一系列已知的规则对查询执行检测。若是查询匹配一种模式的规则,查询就会按照这条规则来重写。下面是(可选)规则的非详尽的列表:
例如:
MySQL
1 2 3 4 5 6 |
SELECT PERSON.* FROM PERSON WHERE PERSON.person_key IN (SELECT MAILS.person_key FROM MAILS WHERE MAILS.mail LIKE 'christophe%'); |
会转换为:
MySQL
1 2 3 4 |
SELECT PERSON.* FROM PERSON, MAILS WHERE PERSON.person_key = MAILS.person_key and MAILS.mail LIKE 'christophe%'; |
【译者注: 物化视图 。谓词,predicate,条件表达式的求值返回真或假的过程】
重写后的查询接着送到优化器,这时候好玩的就开始了。
研究数据库如何优化查询以前咱们须要谈谈统计,由于没有统计的数据库是愚蠢的。除非你明确指示,数据库是不会分析本身的数据的。没有分析会致使数据库作出(很是)糟糕的假设。
可是,数据库须要什么类型的信息呢?
我必须(简要地)谈谈数据库和操做系统如何保存数据。二者使用的最小单位叫作页或块(默认 4 或 8 KB)。这就是说若是你仅须要 1KB,也会占用一个页。要是页的大小为 8KB,你就浪费了 7KB。
回来继续讲统计! 当你要求数据库收集统计信息,数据库会计算下列值:
这些统计信息会帮助优化器估计查询所需的磁盘 I/O、CPU、和内存使用
对每一个列的统计很是重要。
好比,若是一个表 PERSON 须要联接 2 个列: LAST_NAME, FIRST_NAME。
根据统计信息,数据库知道FIRST_NAME只有 1,000 个不一样的值,LAST_NAME 有 1,000,000 个不一样的值。
所以,数据库就会按照 LAST_NAME, FIRST_NAME 联接。
由于 LAST_NAME 不大可能重复,多数状况下比较 LAST_NAME 的头 2 、 3 个字符就够了,这将大大减小比较的次数。
不过,这些只是基本的统计。你可让数据库作一种高级统计,叫直方图。直方图是列值分布状况的统计信息。例如:
这些额外的统计会帮助数据库找到更佳的查询计划,尤为是对于等式谓词(例如: WHERE AGE = 18 )或范围谓词(例如: WHERE AGE > 10 and AGE < 40),由于数据库能够更好的了解这些谓词相关的数字类型数据行(注:这个概念的技术名称叫选择率)。
统计信息保存在数据库元数据内,例如(非分区)表的统计信息位置:
统计信息必须及时更新。若是一个表有 1,000,000 行而数据库认为它只有 500 行,没有比这更糟糕的了。统计惟一的不利之处是须要时间来计算,这就是为何数据库大多默认状况下不会自动计算统计信息。数据达到百万级时统计会变得困难,这时候,你能够选择仅作基本统计或者在一个数据库样本上执行统计。
举个例子,我参与的一个项目须要处理每表上亿条数据的库,我选择只统计10%,结果形成了巨大的时间消耗。本例证实这是个糟糕的决定,由于有时候 Oracle 10G 从特定表的特定列中选出的 10% 跟所有 100% 有很大不一样(对于拥有一亿行数据的表,这种状况极少发生)。此次错误的统计致使了一个本应 30 秒完成的查询最后执行了 8 个小时,查找这个现象根源的过程简直是个噩梦。这个例子显示了统计的重要性。
注:固然了,每一个数据库还有其特定的更高级的统计。若是你想了解更多信息,读读数据库的文档。话虽然这么说,我已经尽力理解统计是如何使用的了,并且我找到的最好的官方文档来自PostgreSQL。
全部的现代数据库都在用基于成本的优化(即CBO)来优化查询。道理是针对每一个运算设置一个成本,经过应用成本最低廉的一系列运算,来找到最佳的下降查询成本的方法。
为了理解成本优化器的原理,我以为最好用个例子来『感觉』一下这个任务背后的复杂性。这里我将给出联接 2 个表的 3 个方法,咱们很快就能看到即使一个简单的联接查询对于优化器来讲都是个噩梦。以后,咱们会了解真正的优化器是怎么作的。
对于这些联接操做,我会专一于它们的时间复杂度,可是,数据库优化器计算的是它们的 CPU 成本、磁盘 I/O 成本、和内存需求。时间复杂度和 CPU 成本的区别是,时间成本是个近似值(给我这样的懒家伙准备的)。而 CPU 成本,我这里包括了全部的运算,好比:加法、条件判断、乘法、迭代……还有呢:
使用时间复杂度就容易多了(至少对我来讲),用它我也能了解到 CBO 的概念。因为磁盘 I/O 是个重要的概念,我偶尔也会提到它。请牢记,大多数时候瓶颈在于磁盘 I/O 而不是 CPU 使用。
在研究 B+树的时候咱们谈到了索引,要记住一点,索引都是已经排了序的。
仅供参考:还有其余类型的索引,好比位图索引,在 CPU、磁盘I/O、和内存方面与B+树索引的成本并不相同。
另外,不少现代数据库为了改善执行计划的成本,能够仅为当前查询动态地生成临时索引。
注:因为全部存取路径的真正问题是磁盘 I/O,我不会过多探讨时间复杂度。
【译者注:四种类型的Oracle索引扫描介绍 】
若是你读过执行计划,必定看到过『全扫描』(或只是『扫描』)一词。简单的说全扫描就是数据库完整的读一个表或索引。就磁盘 I/O 而言,很明显全表扫描的成本比索引全扫描要高昂。
其余类型的扫描有索引范围扫描,好比当你使用谓词 ” WHERE AGE > 20 AND AGE < 40 ” 的时候它就会发生。
固然,你须要在 AGE 字段上有索引才能用到索引范围扫描。
在第一部分咱们已经知道,范围查询的时间成本大约是 log(N)+M,这里 N 是索引的数据量,M 是范围内估测的行数。多亏有了统计咱们才能知道 N 和 M 的值(注: M 是谓词 “ AGE > 20 AND AGE < 40 ” 的选择率)。另外范围扫描时,你不须要读取整个索引,所以在磁盘 I/O 方面没有全扫描那么昂贵。
若是你只须要从索引中取一个值你能够用惟一扫描。
多数状况下,若是数据库使用索引,它就必须查找与索引相关的行,这样就会用到根据 ROW ID 存取的方式。
例如,假如你运行:
MySQL
1 |
SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28 |
若是 person 表的 age 列有索引,优化器会使用索引找到全部年龄为 28 的人,而后它会去表中读取相关的行,这是由于索引中只有 age 的信息而你要的是姓和名。
可是,假如你换个作法:
MySQL
1 2 |
SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON WHERE PERSON.AGE = TYPE_PERSON.AGE |
PERSON 表的索引会用来联接 TYPE_PERSON 表,可是 PERSON 表不会根据行ID 存取,由于你并无要求这个表内的信息。
虽然这个方法在少许存取时表现很好,这个运算的真正问题实际上是磁盘 I/O。假如须要大量的根据行ID存取,数据库也许会选择全扫描。
我没有列举全部的存取路径,若是你感兴趣能够读一读 Oracle文档。其它数据库里也许叫法不一样但背后的概念是同样的。
那么,咱们知道如何获取数据了,那如今就把它们联接起来!
我要展示的是3个个经常使用联接运算符:合并联接(Merge join),哈希联接(Hash Join)和嵌套循环联接(Nested Loop Join)。可是在此以前,我须要引入新词汇了:内关系和外关系( inner relation and outer relation) 【译者注: “内关系和外关系” 这个说法来源不明,跟查询的“内联接(INNER JOIN) 、外联接(OUTER JOIN) ” 不是一个概念 。只查到百度百科词条:关系数据库 里提到“每一个表格(有时被称为一个关系)……” 。 其余参考连接 “Merge Join” “Hash Join” “Nested Loop Join” 】 。 一个关系能够是:
当你联接两个关系时,联接算法对两个关系的处理是不一样的。在本文剩余部分,我将假定:
好比, A JOIN B 是 A 和 B 的联接,这里 A 是外关系,B 是内关系。
多数状况下, A JOIN B 的成本跟 B JOIN A 的成本是不一样的。
在这一部分,我还将假定外关系有 N 个元素,内关系有 M 个元素。要记住,真实的优化器经过统计知道 N 和 M 的值。
注:N 和 M 是关系的基数。【译者注: 基数 】
嵌套循环联接是最简单的。
道理以下:
下面是伪代码:
C
1 2 3 4 5 6 7 8 |
nested_loop_join(array outer, array inner) for each row a in outer for each row b in inner if (match_join_condition(a,b)) write_result_in_output(a,b) end if end for end for |
因为这是个双迭代,时间复杂度是 O(N*M)。
在磁盘 I/O 方面, 针对 N 行外关系的每一行,内部循环须要从内关系读取 M 行。这个算法须要从磁盘读取 N+ N*M 行。可是,若是内关系足够小,你能够把它读入内存,那么就只剩下 M + N 次读取。这样修改以后,内关系必须是最小的,由于它有更大机会装入内存。
在CPU成本方面没有什么区别,可是在磁盘 I/O 方面,最好最好的,是每一个关系只读取一次。
固然,内关系能够由索引代替,对磁盘 I/O 更有利。
因为这个算法很是简单,下面这个版本在内关系太大没法装入内存时,对磁盘 I/O 更加有利。道理以下:
可能的算法以下:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// improved version to reduce the disk I/O. nested_loop_join_v2(file outer, file inner) for each bunch ba in outer // ba is now in memory for each bunch bb in inner // bb is now in memory for each row a in ba for each row b in bb if (match_join_condition(a,b)) write_result_in_output(a,b) end if end for end for end for end for |
使用这个版本,时间复杂度没有变化,可是磁盘访问下降了:
哈希联接更复杂,不过在不少场合比嵌套循环联接成本低。
哈希联接的道理是:
在时间复杂度方面我须要作些假设来简化问题:
时间复杂度是 (M/X) * (N/X) + 建立哈希表的成本(M) + 哈希函数的成本 * N 。
若是哈希函数建立了足够小规模的哈希桶,那么复杂度就是 O(M+N)。
还有个哈希联接的版本,对内存有利可是对磁盘 I/O 不够有利。 这回是这样的:
合并联接是惟一产生排序的联接算法。
注:这个简化的合并联接不区份内表或外表;两个表扮演一样的角色。可是真实的实现方式是不一样的,好比当处理重复值时。
1.(可选)排序联接运算:两个输入源都按照联接关键字排序。
2.合并联接运算:排序后的输入源合并到一块儿。
咱们已经谈到过合并排序,在这里合并排序是个很好的算法(可是并不是最好的,若是内存足够用的话,仍是哈希联接更好)。
然而有时数据集已经排序了,好比:
这部分与咱们研究过的合并排序中的合并运算很是类似。不过这一次呢,咱们不是从两个关系里挑选全部元素,而是只挑选相同的元素。道理以下:
由于两个关系都是已排序的,你不须要『回头去找』,因此这个方法是有效的。
该算法是个简化版,由于它没有处理两个序列中相同数据出现屡次的状况(即多重匹配)。真实版本『仅仅』针对本例就更加复杂,因此我才选择简化版。
若是两个关系都已经排序,时间复杂度是 O(N+M)
若是两个关系须要排序,时间复杂度是对两个关系排序的成本:O(N*Log(N) + M*Log(M))
对于计算机极客,我给出下面这个可能的算法来处理多重匹配(注:对于这个算法我不保证100%正确):
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
mergeJoin(relation a, relation b) relation output integer a_key:=0; integer b_key:=0;
while (a[a_key]!=null and b[b_key]!=null) if (a[a_key] < b[b_key]) a_key++; else if (a[a_key] > b[b_key]) b_key++; else //Join predicate satisfied write_result_in_output(a[a_key],b[b_key]) //We need to be careful when we increase the pointers if (a[a_key+1] != b[b_key]) b_key++; end if if (b[b_key+1] != a[a_key]) a_key++; end if if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1]) b_key++; a_key++; end if end if end while |
若是有最好的,就不必弄那么多种类型了。这个问题很难,由于不少因素都要考虑,好比:
想要更详细的信息,能够阅读DB2, ORACLE 或 SQL Server)的文档。
咱们已经研究了 3 种类型的联接操做。
如今,好比说咱们要联接 5 个表,来得到一我的的所有信息。一我的能够有:
换句话说,咱们须要用下面的查询快速获得答案:
MySQL
1 2 3 4 5 6 |
SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS WHERE PERSON.PERSON_ID = MOBILES.PERSON_ID AND PERSON.PERSON_ID = MAILS.PERSON_ID AND PERSON.PERSON_ID = ADRESSES.PERSON_ID AND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID |
做为一个查询优化器,我必须找处处理数据最好的方法。但有 2 个问题:
那么下面就是我可能采起的方法:
在这个简单的例子中,我最后获得不少可能性。但现实世界的查询还会有其余关系运算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 这意味着更多的可能性。
那么,数据库是如何处理的呢?
动态编程,贪婪算法和启发式算法
关系型数据库会尝试我刚刚提到的多种方法,优化器真正的工做是在有限时间里找到一个好的解决方案。
多数时候,优化器找到的不是最佳的方案,而是一个『不错』的
对于小规模的查询,采起粗暴的方式是有可能的。可是为了让中等规模的查询也能采起粗暴的方式,咱们有办法避免没必要要的计算,这就是动态编程。
这几个字背后的理念是,不少执行计划是很是类似的。看看下图这几种计划:
它们都有相同的子树(A JOIN B),因此,没必要在每一个计划中计算这个子树的成本,计算一次,保存结果,当再遇到这个子树时重用。用更正规的说法,咱们面对的是个重叠问题。为了不对部分结果的重复计算,咱们使用记忆法。
对于计算机极客,下面是我在先前给你的教程里找到的一个算法。我不提供解释,因此仅在你已经了解动态编程或者精通算法的状况下阅读(我提醒过你哦):
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure findbestplan(S) if (bestplan[S].cost infinite) return bestplan[S] // else bestplan[S] has not been computed earlier, compute it now if (S contains only 1 relation) set bestplan[S].plan and bestplan[S].cost based on the best way of accessing S /* Using selections on S and indices on S */ else for each non-empty subset S1 of S such that S1 != S P1= findbestplan(S1) P2= findbestplan(S - S1) A = best algorithm for joining results of P1 and P2 cost = P1.cost + P2.cost + cost of A if cost < bestplan[S].cost bestplan[S].cost = cost bestplan[S].plan = 『execute P1.plan; execute P2.plan; join results of P1 and P2 using A』 return bestplan[S] |
针对大规模查询,你也能够用动态编程方法,可是要附加额外的规则(或者称为启发式算法)来减小可能性。
可是,优化器面对一个很是大的查询,或者为了尽快找到答案(然而查询速度就快不起来了),会应用另外一种算法,叫贪婪算法。
原理是按照一个规则(或启发)以渐进的方式制定查询计划。在这个规则下,贪婪算法逐步寻找最佳算法,先处理一条JOIN,接着每一步按照一样规则加一条新的JOIN。
咱们来看个简单的例子。好比一个针对5张表(A,B,C,D,E)4次JOIN 的查询,为了简化咱们把嵌套JOIN做为可能的联接方式,按照『使用最低成本的联接』规则。
由于咱们是武断地从表 A 开始,咱们能够把一样的算法用在 B,而后 C,而后 D, 而后 E。最后保留成本最低的执行计划。
顺便说一句,这个算法有个名字,叫『最近邻居算法』。
抛开细节不谈,只需一个良好的模型和一个 N*log(N) 复杂度的排序,问题就轻松解决了。这个算法的复杂度是 O(N*log(N)) ,对比一下彻底动态编程的 O(3^N)。若是你有个20个联接的大型查询,这意味着 26 vs 3,486,784,401 ,天壤之别!
这个算法的问题是,咱们作的假设是:找到 2 个表的最佳联接方法,保留这个联接结果,再联接下一个表,就能获得最低的成本。可是:
为了改善这一情况,你能够屡次使用基于不一样规则的贪婪算法,并保留最佳的执行计划。
[ 若是你已经受够了算法话题,就直接跳到下一部分。这部分对文章余下的内容不重要。]【译者注:我也很想把这段跳过去 -_- 】
不少计算机科学研究者热衷于寻找最佳的执行计划,他们常常为特定问题或模式探寻更好的解决方案,好比:
其余算法也在研究之中,就是为了替换在大型查询中的动态编程算法。贪婪算法属于一个叫作启发式算法的你们族,它根据一条规则(或启发),保存上一步找到的方法,『附加』到当前步骤来进一步搜寻解决方法。有些算法根据特定规则,一步步的应用规则但不老是保留上一步找到的最佳方法。它们统称启发式算法。
好比,基因算法就是一种:
循环次数越多,计划就越好。
这是魔术?不,这是天然法则:适者生存!
PostgreSQL 实现了基因算法,但我并无发现它是否是默认使用这种算法的。
数据库中还使用了其它启发式算法,像『模拟退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『双阶段优化算法(Two-Phase Optimization)』…..不过,我不知道这些算法当前是否在企业级数据库应用了,仍是仅仅用在研究型数据库。
若是想进一步了解,这篇研究文章介绍两个更多可能的算法《数据库查询优化中联接排序问题的算法综述》,你能够去阅读一下。
[ 这段不重要,能够跳过 ]
然而,全部上述罗里罗嗦的都很是理论化,我是个开发者而不是研究者,我喜欢具体的例子。
咱们来看看 SQLite 优化器 是怎么工做的。这是个轻量化数据库,它使用一种简单优化器,基于带有附加规则的贪婪算法,来限制可能性的数量。
咱们再看看另外一个优化器是怎么工做的。IBM DB2 跟全部企业级数据库都相似,我讨论它是由于在切换到大数据以前,它是我最后真正使用的数据库。
看过官方文档后,咱们了解到 DB2 优化器可让你使用 7 种级别的优化:
能够看到 DB2 使用贪婪算法和动态编程算法。固然,他们不会把本身的启发算法分享出来的,由于查询优化器是数据库的看家本领。
DB2 的默认级别是 5,优化器使用下列特性: 【译者注:如下出现的一些概念我没有作考证,由于[ 这段不重要,能够跳过 ]】
默认的,DB2 对联接排列使用受启发式限制的动态编程算法。
其它状况 (GROUP BY, DISTINCT…) 由简单规则处理。
因为建立查询计划是耗时的,大多数据库把计划保存在查询计划缓存,来避免重复计算。这个话题比较大,由于数据库须要知道何时更新过期的计划。办法是设置一个上限,若是一个表的统计变化超过了上限,关于该表的查询计划就从缓存中清除。
在这个阶段,咱们有了一个优化的执行计划,再编译为可执行代码。而后,若是有足够资源(内存,CPU),查询执行器就会执行它。计划中的操做符 (JOIN, SORT BY …) 能够顺序或并行执行,这取决于执行器。为了得到和写入数据,查询执行器与数据管理器交互,本文下一部分来讨论数据管理器。
在这一步,查询管理器执行了查询,须要从表和索引获取数据,因而向数据管理器提出请求。可是有 2 个问题:
在这一部分,我没看看关系型数据库是如何处理这两个问题的。我不会讲数据管理器是怎么得到数据的,由于这不是最重要的(并且本文已经够长的了!)。
我已经说过,数据库的主要瓶颈是磁盘 I/O。为了提升性能,现代数据库使用缓存管理器。
查询执行器不会直接从文件系统拿数据,而是向缓存管理器要。缓存管理器有一个内存缓存区,叫作缓冲池,从内存读取数据显著地提高数据库性能。对此很难给出一个数量级,由于这取决于你须要的是哪一种操做:
以及数据库使用的磁盘类型:
要我说,内存比磁盘要快100到10万倍。
然而,这致使了另外一个问题(数据库老是这样…),缓存管理器须要在查询执行器使用数据以前获得数据,不然查询管理器不得不等待数据从缓慢的磁盘中读出来。
这个问题叫预读。查询执行器知道它将须要什么数据,由于它了解整个查询流,并且经过统计也了解磁盘上的数据。道理是这样的:
缓存管理器在缓冲池里保存全部的这些数据。为了肯定一条数据是否有用,缓存管理器给缓存的数据添加了额外的信息(叫闩锁)。
有时查询执行器不知道它须要什么数据,有的数据库也不提供这个功能。相反,它们使用一种推测预读法(好比:若是查询执行器想要数据一、三、5,它不久后极可能会要 七、九、11),或者顺序预读法(这时候缓存管理器只是读取一批数据后简单地从磁盘加载下一批连续数据)。
为了监控预读的工做情况,现代数据库引入了一个度量叫缓冲/缓存命中率,用来显示请求的数据在缓存中找到而不是从磁盘读取的频率。
注:糟糕的缓存命中率不老是意味着缓存工做状态不佳。更多信息请阅读Oracle文档。
缓冲只是容量有限的内存空间,所以,为了加载新的数据,它须要移除一些数据。加载和清除缓存须要一些磁盘和网络I/O的成本。若是你有个常常执行的查询,那么每次都把查询结果加载而后清除,效率就过低了。现代数据库用缓冲区置换策略来解决这个问题。
多数现代数据库(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 算法。
LRU表明最近最少使用(Least Recently Used)算法,背后的原理是:在缓存里保留的数据是最近使用的,因此更有可能再次使用。
图解:
为了更好的理解,我假设缓冲区里的数据没有被闩锁锁住(就是说是能够被移除的)。在这个简单的例子里,缓冲区能够保存 3 个元素:
这个算法效果很好,可是有些限制。若是对一个大表执行全表扫描怎么办?换句话说,当表/索引的大小超出缓冲区会发生什么?使用这个算法会清除以前缓存内全部的数据,并且全扫描的数据极可能只使用一次。
为了防止这个现象,有些数据库增长了特殊的规则,好比Oracle文档中的描述:
『对很是大的表来讲,数据库一般使用直接路径来读取,即直接加载区块[……],来避免填满缓冲区。对于中等大小的表,数据库能够使用直接读取或缓存读取。若是选择缓存读取,数据库把区块置于LRU的尾部,防止清空当前缓冲区。』
还有一些可能,好比使用高级版本的LRU,叫作 LRU-K。例如,SQL Server 使用 LRU-2。
这个算法的原理是把更多的历史记录考虑进来。简单LRU(也就是 LRU-1),只考虑最后一次使用的数据。LRU-K呢:
计算权重是须要成本的,因此SQL Server只是使用 K=2,这个值性能不错并且额外开销能够接受。
关于LRU-K更深刻的知识,能够阅读早期的研究论文(1993):数据库磁盘缓冲的LRU-K页面置换算法
其余算法
固然还有其余管理缓存的算法,好比:
我只探讨了读缓存 —— 在使用以前预先加载数据。用来保存数据、成批刷入磁盘,而不是逐条写入数据从而形成不少单次磁盘访问。
要记住,缓冲区保存的是页(最小的数据单位)而不是行(逻辑上/人类习惯的观察数据的方式)。缓冲池内的页若是被修改了但尚未写入磁盘,就是脏页。有不少算法来决定写入脏页的最佳时机,但这个问题与事务的概念高度关联,下面咱们就谈谈事务。
最后但一样重要的,是事务管理器,咱们将看到这个进程是如何保证每一个查询在本身的事务内执行的。但开始以前,咱们须要理解ACID事务的概念。
一个ACID事务是一个工做单元,它要保证4个属性:
在同一个事务内,你能够运行多个SQL查询来读取、建立、更新和删除数据。当两个事务使用相同的数据,麻烦就来了。经典的例子是从帐户A到帐户B的汇款。假设有2个事务:
咱们回来看看ACID属性:
[如下部分不重要,能够跳过]
现代数据库不会使用纯粹的隔离做为默认模式,由于它会带来巨大的性能消耗。SQL通常定义4个隔离级别:
多数数据库添加了自定义的隔离级别(好比 PostgreSQL、Oracle、SQL Server的快照隔离),并且并无实现SQL规范里的全部级别(尤为是读取未提交级别)。
默认的隔离级别能够由用户/开发者在创建链接时覆盖(只须要增长很简单的一行代码)。
确保隔离性、一致性和原子性的真正问题是对相同数据的写操做(增、更、删):
这个问题叫并发控制。
最简单的解决办法是依次执行每一个事务(即顺序执行),但这样就彻底没有伸缩性了,在一个多处理器/多核服务器上只有一个核心在工做,效率很低。
理想的办法是,每次一个事务建立或取消时:
用更正规的说法,这是对冲突的调度问题。更具体点儿说,这是个很是困难并且CPU开销很大的优化问题。企业级数据库没法承担等待几个小时,来寻找每一个新事务活动最好的调度,所以就使用不那么理想的方式以免更多的时间浪费在解决冲突上。
为了解决这个问题,多数数据库使用锁和/或数据版本控制。这是个很大的话题,我会集中探讨锁,和一点点数据版本控制。
原理是:
可是对一个仅仅读取数据的事务使用排他锁很是昂贵,由于这会迫使其它只须要读取相同数据的事务等待。所以就有了另外一种锁,共享锁。
共享锁是这样的:
一样的,若是一块数据被加上排他锁,一个只须要读取该数据的事务必须等待排他锁释放才能给该数据加上共享锁。
锁管理器是添加和释放锁的进程,在内部用一个哈希表保存锁信息(关键字是被锁的数据),而且了解每一块数据是:
可是使用锁会致使一种状况,2个事务永远在等待一块数据:
在本图中:
这叫死锁。
在死锁发生时,锁管理器要选择取消(回滚)一个事务,以便消除死锁。这但是个艰难的决定:
在做出选择以前,锁管理器须要检查是否有死锁存在。
哈希表能够看做是个图表(见上文图),图中出现循环就说明有死锁。因为检查循环是昂贵的(全部锁组成的图表是很庞大的),常常会经过简单的途径解决:使用超时设定。若是一个锁在超时时间内没有加上,那事务就进入死锁状态。
锁管理器也能够在加锁以前检查该锁会不会变成死锁,可是想要完美的作到这一点仍是很昂贵的。所以这些预检常常设置一些基本规则。
两段锁
实现纯粹的隔离最简单的方法是:事务开始时获取锁,结束时释放锁。就是说,事务开始前必须等待确保本身能加上全部的锁,当事务结束时释放本身持有的锁。这是行得通的,可是为了等待全部的锁,大量的时间被浪费了。
更快的方法是两段锁协议(Two-Phase Locking Protocol,由 DB2 和 SQL Server使用),在这里,事务分为两个阶段:
这两条简单规则背后的原理是:
这个规则能够很好地工做,但有个例外:若是修改了一条数据、释放了关联的锁后,事务被取消(回滚),而另外一个事务读到了修改后的值,但最后这个值却被回滚。为了不这个问题,全部独占锁必须在事务结束时释放。
多说几句
固然了,真实的数据库使用更复杂的系统,涉及到更多类型的锁(好比意向锁,intention locks)和更多的粒度(行级锁、页级锁、分区锁、表锁、表空间锁),可是道理是相同的。
我只探讨纯粹基于锁的方法,数据版本控制是解决这个问题的另外一个方法。
版本控制是这样的:
这将提升性能,由于:
除了两个事务写相同数据的时候,数据版本控制各个方面都比锁表现得更好。只不过,你很快就会发现磁盘空间消耗巨大。
数据版本控制和锁机制是两种不一样的看法:乐观锁和悲观锁。二者各有利弊,彻底取决于使用场景(读多仍是写多)。关于数据版本控制,我推荐这篇很是优秀的文章,讲的是PostgreSQL如何实现多版本并发控制的。
一些数据库,好比DB2(直到版本 9.7)和 SQL Server(不含快照隔离)仅使用锁机制。其余的像PostgreSQL, MySQL 和 Oracle 使用锁和鼠标版本控制混合机制。我不知道是否有仅用版本控制的数据库(若是你知道请告诉我)。
[2015-08-20更新]一名读者告诉我:
Firebird 和 Interbase 用不带锁的版本控制。
版本控制对索引的影响挺有趣的:有时惟一索引会出现重复,索引的条目会多于表行数,等等。
若是你读过不一样级别的隔离那部份内容,你会知道,提升隔离级别就会增长锁的数量和事务等待加锁的时间。这就是为何多数数据库默认不会使用最高级别的隔离(即串行化)。
固然,你老是能够本身去主流数据库(像MySQL, PostgreSQL 或 Oracle)的文档里查一下。
日志管理器
咱们已经知道,为了提高性能,数据库把数据保存在内存缓冲区内。但若是当事务提交时服务器崩溃,崩溃时还在内存里的数据会丢失,这破坏了事务的持久性。
你能够把全部数据都写在磁盘上,可是若是服务器崩溃,最终数据可能只有部分写入磁盘,这破坏了事务的原子性。
事务做出的任何修改必须是或者撤销,或者完成。
有 2 个办法解决这个问题:
影子副本/页在运行较多事务的大型数据库时制造了大量磁盘开销,因此现代数据库使用事务日志。事务日志必须保存在稳定的存储上,我不会深挖存储技术,但至少RAID磁盘是必须的,以防磁盘故障。
多数数据库(至少是Oracle, SQL Server, DB2, PostgreSQL, MySQL 和 SQLite) 使用预写日志协议(Write-Ahead Logging protocol ,WAL)来处理事务日志。WAL协议有 3 个规则:
这个工做由日志管理器完成。简单的理解就是,日志管理器处于缓存管理器(cache manager)和数据访问管理器(data access manager,负责把数据写入磁盘)之间,每一个 update / delete / create / commit / rollback 操做在写入磁盘以前先写入事务日志。简单,对吧?
回答错误! 咱们研究了这么多内容,如今你应该知道与数据库相关的每一件事都带着『数据库效应』的诅咒。好吧,咱们说正经的,问题在于,如何找到写日志的同时保持良好的性能的方法。若是事务日志写得太慢,总体都会慢下来。
ARIES
1992年,IBM 研究人员『发明』了WAL的加强版,叫 ARIES。ARIES 或多或少地在现代数据库中使用,逻辑未必相同,但AIRES背后的概念无处不在。我给发明加了引号是由于,按照MIT这门课的说法,IBM 的研究人员『仅仅是写了事务恢复的最佳实践方法』。AIRES 论文发表的时候我才 5 岁,我不关心那些酸溜溜的科研人员老掉牙的闲言碎语。事实上,我说起这个典故,是在开始探讨最后一个技术点前让你轻松一下。我阅读过这篇 ARIES 论文 的大量篇幅,发现它颇有趣。在这一部分我只是简要的谈一下 ARIES,不过我强烈建议,若是你想了解真正的知识,就去读那篇论文。
ARIES 表明『数据库恢复原型算法』(Algorithms for Recovery and Isolation Exploiting Semantics)。
这个技术要达到一个双重目标:
有多个缘由让数据库不得不回滚事务:
有时候(好比网络出现故障),数据库能够恢复事务。
这怎么可能呢?为了回答这个问题,咱们须要了解日志里保存的信息。
事务的每个操做(增/删/改)产生一条日志,由以下内容组成:
进一步说,磁盘上每一个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操做的LSN。
*LSN的分配其实更复杂,由于它关系到日志存储的方式。但道理是相同的。
** ARIES 只使用逻辑UNDO,由于处理物理UNDO太过混乱了。
注:据我所知,只有 PostgreSQL 没有使用UNDO,而是用一个垃圾回收服务来删除旧版本的数据。这个跟 PostgreSQL 对数据版本控制的实现有关。
为了更好的说明这一点,这有一个简单的日志记录演示图,是由查询 “UPDATE FROM PERSON SET AGE = 18;” 产生的,咱们假设这个查询是事务18执行的。【译者注: SQL 语句原文如此,应该是做者笔误 】
每条日志都有一个惟一的LSN,连接在一块儿的日志属于同一个事务。日志按照时间顺序连接(连接列表的最后一条日志是最后一个操做产生的)。
为了防止写日志成为主要的瓶颈,数据库使用了日志缓冲区。
当查询执行器要求作一次修改:
当事务提交,意味着事务每个操做的 1 2 3 4 5 步骤都完成了。写事务日志是很快的,由于它只是『在事务日志某处增长一条日志』;而数据写盘就更复杂了,由于要用『可以快速读取的方式写入数据』。
出于性能方面的缘由,第 5 步有可能在提交以后完成,由于一旦发生崩溃,还有可能用REDO日志恢复事务。这叫作 NO-FORCE策略。
数据库能够选择FORCE策略(好比第 5 步在提交以前必须完成)来下降恢复时的负载。
另外一个问题是,要选择数据是一步步的写入(STEAL策略),仍是缓冲管理器须要等待提交命令来一次性所有写入(NO-STEAL策略)。选择STEAL仍是NO-STEAL取决于你想要什么:快速写入可是从 UNDO 日志恢复缓慢,仍是快速恢复。
总结一下这些策略对恢复的影响:
Ok,有了不错的日志,咱们来用用它们!
假设新来的实习生让数据库崩溃了(首要规矩:永远是实习生的错。),你重启了数据库,恢复过程开始了。
ARIES从崩溃中恢复有三个阶段:
在REDO阶段,REDO日志按照时间顺序处理(使用LSN)。
对每一条日志,恢复进程须要读取包含数据的磁盘页LSN。
若是LSN(磁盘页)>= LSN(日志记录),说明数据已经在崩溃前写到磁盘(可是值已经被日志以后、崩溃以前的某个操做覆盖),因此不须要作什么。
若是LSN(磁盘页)< LSN(日志记录),那么磁盘上的页将被更新。
即便将被回滚的事务,REDO也是要作的,由于这样简化了恢复过程(可是我相信现代数据库不会这么作的)。
恢复过程当中,事务日志必须留意恢复过程的操做,以便写入磁盘的数据与事务日志相一致。一个解决办法是移除被取消的事务产生的日志记录,可是这个太困难了。相反,ARIES在事务日志中记录补偿日志,来逻辑上删除被取消的事务的日志记录。
当事务被『手工』取消,或者被锁管理器取消(为了消除死锁),或仅仅由于网络故障而取消,那么分析阶段就不须要了。对于哪些须要 REDO 哪些须要 UNDO 的信息在 2 个内存表中:
当新的事务产生时,这两个表由缓存管理器和事务管理器更新。由于是在内存中,当数据库崩溃时它们也被破坏掉了。
分析阶段的任务就是在崩溃以后,用事务日志中的信息重建上述的两个表。为了加快分析阶段,ARIES提出了一个概念:检查点(check point),就是不时地把事务表和脏页表的内容,还有此时最后一条LSN写入磁盘。那么在分析阶段当中,只须要分析这个LSN以后的日志便可。
写这篇文章以前,我知道这个题目有多大,也知道写这样一篇深刻的文章会至关耗时。最后证实我过于乐观了,实际上花了两倍于预期的时间,可是我学到了不少。
若是你想很好地了解数据库,我推荐这篇研究论文:《数据库系统架构》,对数据库有很好的介绍(共110页),并且非计算机专业人士也能读懂。这篇论文出色的帮助我制定了本文的写做计划,它没有像本文那样专一于数据结构和算法,更多的讲了架构方面的概念。
若是你仔细阅读了本文,你如今应该了解一个数据库是多么的强大了。鉴于文章很长,让我来提醒你咱们都学到了什么:
可是,数据库包含了更多的聪明巧技。好比,我并无谈到下面这些棘手的问题:
因此,当你不得不在问题多多的 NoSQL数据库和坚如磐石的关系型数据库之间抉择的时候,要三思而行。不要误会,某些 NoSQL数据库是很棒的,可是它们毕竟还年轻,只是解决了少许应用关注的一些特定问题。