首先:感谢Christophe Kalenzaga,他对数据库的不了解,而想去了解这个对不少程序员来讲的黑盒子,阅读了大量的文章,官方文档,研究资料,完成了本文。 本文的源地址在()javascript
当提到关系数据库,情不自禁地认为缺乏了些重要信息。这些数据库无所不在,发挥着他们的做用,他们包括了小巧的sqlite,强大的 Teradat。可是少有文章去说明数据的运行原理。你能够搜索 “how does a relational database work”【关系数据库运行原理】来了解这样的文章有多么少。若是你去搜索如今这些流行技术(大数据,Nosql和javascript),你会找到大量 深刻的文章在说明这些技术的运行原理。关系数据库太不流行太没意思,以致于出了大学课堂,就找不到书和研究资料去阐明它的运行原理?
html
做为一个开发者,我很是不喜欢用一些我不理解的技术/组件。即便数据库已经通过了40年运用的检验,可是我依然不喜欢。这些年,我花费了上百小时的时间去研究这些天天都用的奇怪的黑匣子。关系型数据库的有趣也由于他门构建在有效和重用的理念上。若是你要了解数据库,而有没有时间或者没有毅力去了解这个宽泛的话题,那么你应该阅读这篇文章。java
虽然文章标题已经足够明确,本文的目的不是让你学习怎么使用一个数据库.可是,你应该已经知道怎么写一个简单的链接查询和基本的增删改查的查询,不然,你就不能明白本文。这就是如今必需要知道,我将解释为何须要这些提早的知识。git
我将从时间复杂度开始开始这些计算机科学知识。固然固然,我晓得有些朋友不喜欢这些观点可是不了解这些,咱们就不明白数据库中使用的技巧。这是一个庞大的话题,我将聚焦于很是必要的知识上,数据库处理SQL查询的方法。我将只涉及数据库背后的基本观念,让你在本文结束的时候了解水面下发生了什么。程序员
这是一篇又长又有技术性的文章,涉及了不少算法和数据结构,总之不怎么好理解,慢慢看吧同窗。其有一些观点确实不容易理解,你把它跳过去也能获得一 个比较全面的理解(译者注:这篇博文对于学习过《数据结构》的同窗,不算是很难,即便有一些难以理解的观点,要涉及技术的特性,这是使用这些许技的缘由, 对应可以明白使用技术要达成的结果)。github
本文大致分为3个部分,为了方便理解:算法
好久之前(估计有银河系诞生那么久远...),开发人员不得不精通很是多的编程操做。由于他们不能浪费他们龟速电脑上哪怕一丁点儿的CPU和内存,他们必须将这些算法和相应的数据结构深深的记在内心。
在这个部分,我将带大家回忆一些这样的概念,由于它们对于理解数据库是很是必要的。我也将会介绍数据库索引这个概念。
sql
如今,许多开发者再也不关心时间复杂度...他们是对的!
可是当大家正面临着一个大数据量(我谈论的并非几千这个级别的数据)的处理问题时或者正努力为毫秒级的性能提高拼命时,理解这个概念就很是的重要了。可 是大家猜怎么着?数据库不得不处理这两种极端状况!我不会占用大家太多时间,只须要将这个点子讲清楚就够了。这将会帮助咱们之后理解成本导向最优化的概念。
shell
时间复杂度时用来衡量一个算法处理给定量的数据所消耗时间多少的。为了描述这个复琐事物,计算机科学家们用数学上的大写字母O符号.这个符号用来描述了在方法中一个算法须要多少次操做才能处理完给定的输入数据量。
例如,当我说”这个算法是在O(some_funtion())“时,这意味着这个算法为了处理肯定量的数据须要执行some_function(a_certain_amount_of_data)操做.
最重要的不是数据量,而是随着数据量的增长,操做步骤须要随之变化的方式。时间复杂度不是给出确切的操做数量而是一个概念。 数据库
在上图中,你能够看到不一样类型的复杂度演变的方式。我用了对数尺度来描绘。换句话讲,当数据的量从1到10亿,咱们能够看到:
当小数据量时,O(1)与O(n2)之间的差距是微乎其微的。例如,假设你须要处理2000条数据的算法。
O(1)与O(n2)之间的区别彷佛很是大(4百万倍),可是你实际上最多多消耗2毫秒,和你眨眼的时间几乎相同。的确,如今的处理器能处理每秒数以百万计指令
。这就是为何在许多IT工程中性能和优化并非主要问题的缘由。
正如我所说,当面对海量数据时,了解这个概念仍是很是重要的。若是这时算法须要处理1000000条数据(对于数据库来讲,这还不算大):
我没有详细算过,可是我想若是采用O(n2)算法,你能够有时间来杯咖啡了(甚至再来一杯!)。若是你又将数据数量级提高一个0,你能够有时间去打个盹儿了。
给你一个概念:
注意:在以后的内容中,咱们将会看到这些算法和数据结构。
存在着多种种类的时间复杂度:
时间复杂度常常是最差状况。
我仅讨论时间复杂度,实际上复杂度还适用于:
固然也有比n2还差的复杂度状况,例如:
注意:我并无给你O符号的真正定义,而只是抛出这个概念。若是你想找到真正的定义,你能够阅读这篇WikiPedia材料
。
若是你须要排序一个集合,你会怎么作?什么?你会调用sort()函数... 好吧,真是个好答案...可是对于数据库来讲,你必须懂得sort()函数是如何工做的。
由于有太多好的排序算法,因此我将专一于最重要的一个:归并排序。此时此刻你可能不是很明白为何数据排序会有用,可是当完成这个部分的查询优化后,你确定会懂得。进一步来讲,掌握归并排序将有助于后续咱们对通常数据库中合并链接操做的理解。
如同许多有用的算法,归并排序是基础的技巧:合并2个长度为N/2的有序数组为一个有N个元素的有序数组仅消耗N次操做。这个操做称为一次合并。
你能从图中看到最终排序好8个元素的数组的结构,你仅须要重复访问一次2个4元素数组。由于这2个4元素数组已经排序好了:
这个算法之因此生效是由于4元素数组都是已经排序好的,所以你没必要在这些数组中进行"回退"。
如今咱们懂得了这个技巧,以下所示是个人合并排序伪代码。
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为底)。
在排序阶段中,你将从单一数组开始。在每一步中,你使用多重聚集。总共须要N=8次操做:
由于总共有log(N)个步骤,总共须要N * log(N)次操做。
为何这个算法有如此威力?
原因以下:
Hadoop
的核心模块(一种大数据框架)。这个排序算法应用于大多数(好吧,若是不是所有)数据库,可是它不是惟一的。若是你想了解更多,你能够阅读这个研究材料,这里面讨论了数据库中所使用到的通用排序算法的优缺点。
咱们已经了解时间复杂度和排序背后的机理,我必须给你讲3种数据结构。它们十分重要,由于它们是现代数据库的支柱。同时我也会介绍数据库索引。
二维数组是最简单的数据结构。表格也能当作是一个数组。以下:
二维数组就是一个行列表:
尽管这样存储和数据可视化都很是好,可是当你面对特殊数据时,这个就很糟了。
例如,若是你想找到全部工做在英国的人,你将不得不查看每一行看这一行是否属于英国。这将消耗你N步操做(N是行数),这并不算太坏,可是否又有更好的方式呢?这就是为何要引入tree。
注意:大多数现代数据库提供了加强型数组来高效的存储表单,例如:堆组织表或索引组织表。可是他并无解决特殊值在列集合中的快速查找问题。
二叉搜索树时一种带有特殊属性的二叉树,每一个节点的键值必须知足:
这棵树有 N=15 个节点构成。假设咱们搜索208:
接下来假设咱们查找40
最后,这两个查询都消耗树的层数次操做。若是你仔细地阅读了归并排序部分,那么就应该知道这里是log(N)层级。因此知道搜索算法的时间复杂度是log(N),不错!
回到咱们的问题上
可是这些东西仍是比较抽象,咱们回到咱们具体的问题中。取代了前一张表中呆滞的整型,假想用字符串来表示某人的国籍。假设你又一棵包含了表格中“国籍”列的树:
这个查找仅须要log(N)次操做而不是像使用数组同样须要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))次操做。更糟的是,添加索引意味着事务管理(咱们将在本文最后看到这个管理)更多的工做量。
更多详情,能够查看维基百科B+树资料
。若是你想知道数据库中B+树的实现细节,请查看来自MySQL核心开发者的博文
和博文
。这两个材料都是聚焦于innoDB(MySQL数据库引擎)如何处理索引。
注意:由于我被一个读者告知,因为低级优化,B+树须要彻底平衡。
咱们最后一个重要的数据结构是哈希表。当你想快速查找值的时候这会很是有用。更好的是了解哈希表有助于掌握数据库通用链接操做中的哈希链接。这个数据结构也用于数据库存储一些中间量(如咱们稍后会提到的锁表或缓冲池概念)。
哈希表是一种利用其键值快速查找元素的数据结构。为了创建哈希表,你须要定义:
一个简单例子
这个哈希表有10个哈希桶。换句话说,我只使用元素的最后一个数字来查找它的哈希桶:
我所使用的比较方法仅仅是简单的比较2个整型是否相等。
假设你想查找一个78的元素:
接下来,假设你想找到59的元素:
优秀的哈希方法
如你所见,根据你查找的值不一样,消耗也是不一样的!
若是如今我改用键值除以1 000 000的哈希方法(也就是取最后6位数字),第二个查找方法仅须要1步操做,由于不存在000059号的哈希桶。真正的挑战是找到一个建立能容纳足够小元素的哈希桶的哈希方法。
在个人例子中,找到一个好的哈希方法是很是容易的。可是因为这是个简单的例子,当面对以下键时,找到哈希方法就很是困难了:
若是有一好的哈希方法,在哈希表中查找的复杂度将是O(1)。
数组与哈希表的比较
为何不使用数组?
嗯, 你问了一个好问题。
想要更多的信息,你能够阅读个人博文一个高效哈希表的实现Java HashMap
;你能够读懂这个文章内容而没必要掌握Java。
咱们已经理解了数据库使用的基本组件,咱们须要回头看看这个整体结构图。
数据库就是一个文件集合,而这里信息能够被方便读写和修改。经过一些文件,也能够达成相同的目的(便于读写和修改)。事实上,一些简单的数据库好比SQLite就仅仅使用了一些文件。可是SQLite是一些通过了良好设计的文件,由于它提供了如下功能:
通常而言,数据库的结构以下图所示:
在开始写以前,我曾经看过不少的书和论文,而这些资料都从本身的方式来讲明数据库。因此,清不要太过关注我怎么组织数据库的结构和对这些过程的命名,由于我选择了这些来配置文章的规划。无论这些不一样模块有多不同,可是他们整体的观点是数据库被划分为多个相互交互的模块。
核心模块:
工具类:
查询管理器:
数据管理器:
本文剩下部分,我将关注于数据库如何处理SQL查询的过程:
客户端管理器是处理和客户端交互的部分。一个客户端多是(网页)服务器或者终端用户或者终端程序。客户端管理器提供不一样的方法(广为人知的API: JDBC, ODBC, OLE-DB)来访问数据库。 固然它也提供数据库特有的数据库APIs。
当咱们链接数据库:
查询过程不是一个all or nothing的过程,当从查询管理器获取数据以后,就马上将这些不彻底的结果存到内存中,并开始传送数据。
当遇到失败,他就中断链接,返回给你一个易读的说明,并释放使用到的资源。
这部分是数据库的重点所在。在本节中,一个写的不怎么好的查询请求将转化成一个飞快执行指令代码。接着执行这个指令代码,并返回结果给客户端管理器。这是一个多步骤的操做。
阅读完这部分以后,你将容易理解我推荐你读的这些材料:
针对PostgreSQL查询优化的很是好的文档。这是很是容易理解的文档,它更展现的是“PostgreSQL在不一样场景下,使用相应的查询计划”,而不是“PostgreSQL使用的算法”。
SQLite关于优化的官方 文档。很是容易阅读,由于SQLite使用的很是简单的规则。此外,这是为惟一一个真正解释如何使用优化规则的文档。
“DATABASE SYSTEM CONCEPTS”做者写的两个关于查询优化的2个理论课程 and
. 关注于磁盘I/O一个很好的读物,可是须要必定的计算机科学功底。
解析器会将每一条SQL语句检验,查看语法正确与否。若是你在SQL语句中犯了一些错误,解析器将阻止这个查询。好比你将"SELECT...."写成了"SLECT ....",此次查询就到此为止了。
说的深一点,他会检查关键字使用先后位置是否正确。好比阻止WHERE 在SELECT以前的查询语句。
以后,查询语句中的表名,字段名要被解析。解析器就要使用数据库的元数据来验证:
以后确认你是否有权限去读/写这些表。再次说明,DBA设置这些读写权限。 在解析过程当中,SQL查询语句将被转换成一个数据库的一种内部表示(通常是树 译者注:ast) 若是一切进行顺利,以后这种表示将会传递给查询重写器
在这一步,咱们已经获得了这个查询内部的表示。重写器的目的在:
重写器执行一系列广为人知的查询规则。若是这个查询匹配了规则的模型,这个规则就要生效,同时重写这个查询。下列有几个(可选的)规则:
例子以下:
SELECT PERSON.* FROM PERSON WHERE PERSON.person_key IN (SELECT MAILS.person_key FROM MAILS WHERE MAILS.mail LIKE 'christophe%');
将会改写成:
SELECT PERSON.* FROM PERSON, MAILS WHERE PERSON.person_key = MAILS.person_key and MAILS.mail LIKE 'christophe%';
这时候,重写的查询传递给查询优化器。 好戏开场了。
在看优化查询以前,咱们必需要说一下统计,由于统计是数据库的智慧之源。若是你不告诉数据如何分析数据库本身的数据,它将不能完成或者进行很是坏的推测。
数据库须要什么样的信息?
我必须简要的谈一下,数据库和操做系统如何存储数据。他们使用一个称为page或者block(一般4K或者8K字节)的最小存储单元。这意味着若是你须要1K字节(须要存储),将要使用一个page。若是一个页大小为8K,你会浪费其余的7K。 注:
计算机内存使用的存储单元为page,文件系统的存储单元成为block
K -> 1024
4K -> 4096
8K -> 8192
继续咱们的统计话题!你须要数据库去收集统计信息,他将会计算这些信息:
table的索引(indexes)信息
这些统计将帮助优化器去计算磁盘IO,CPU和查询使用的内存量
这些每一列的统计是很是重要的,好比:若是一个表 PERSON须要链接(join)两个列:LAST_ANME,RIRST_NAME。有这些统计信息,数据库就会知道RIRST_NAME只有1000 个不一样的值,LAST_NAME不一样的值将会超过100000个。所以,数据库将会链接(join)数据使用LAST_ANME,RIRST_NAME而 不是FIREST_NAME,LAST_NAME,由于LAST_NAME更少的重复,通常比较2-3个字符已经足够区别了。这样就会更少的比较。
这只是基本的统计,你能让数据库计算直方图这种更高级的统计。直方图可以统计列中数据的分布状况。好比:
.....
这些额外的统计将能帮助数据库找到最优的查询计划。特别对等式查询计算(例:WHERE AGE = 18)或者范围查询计算(例:WEHRE AGE > 10 and ARG < 40)由于数据更明白这些查询计算涉及的行数(注:科技界把这种思路叫作选择性)。
这些统计数据存在数据的元数据。好比你能这些统计数据在这些(没有分区的)表中
Oracle的表USER/ALL/DBA_TABLES 和 USER/ALL/DBA_TAB_COLUMNS
这些统计信息必须时时更新。若是出现数据库的表中有1000 000行数据而数据库只认为有500行,那就太糟糕了。统计这些数据有一个缺陷就是:要耗费时间去计算。这就是大多数数据库没有默认自动进行统计计算的缘由。当有数以百万计的数据存在,确实很难进行计算。在这种状况下,你能够选择进行基本统计或者数据中抽样统计一些状态。
好比:我正在进行一个计算表的行数达到亿级的统计工程,即便我只计算其中10%的数据,这也要耗费大量的时间。例子,这不是一个好的决定,由于有时候 Oracle 10G在特定表特定列选择的这10%的数据统计的数据和所有100%统计的数据差异极大(一个表中有一亿行数据是很罕见的)。这就是一个错误的统计将会导 致本来30s的查询却要耗费8个小时;找到致使的缘由也是一个噩梦。这个例子战士了统计是多么的重要。
注:固然每种数据库都有他本身更高级的统计。若是你想知道更多请好好阅读这些数据库的文档。值得一提的是,我之前尝试去了解这些统计是如何用的,我发现了这个最好的官方文档
全部的现代数据库都使用基于成本优化(CBO)的优化技术去优化查询。这个方法认为每个操做都有成本,经过最少成本的操做链获得结果的方式,找到最优的方法去减小每一个查询的成本。
为了明白成本优化器的工做,最好的例子是"感觉"一个任务背后的复杂性。这个部分我将展现3个经常使用方法去链接(join)两个表。咱们会快速明白一个简单链接查询是多么的那一优化。以后,咱们将会看到真正的优化器是如何工做的。
我将关注这些链接查询的时间复杂度而不是数据库优化器计算他们CPU成本,磁盘IO成本和内存使用。时间复杂度和CPU成本区别是,时间复杂度是估算的(这是想我这样懒人的工具)。对于CPU成本,我还要累加每个操做一个加法、一个if语句,一个乘法,一个迭代... 此外:
使用时间复杂度太简单(起码对我来讲)。使用它咱们能轻易明白CBO的思路。咱们须要讨论一下磁盘IO,这个也是一个重要的概念。记住:一般状况,性能瓶颈在磁盘IO而不是CPU使用。
咱们讨论的索引就是咱们看到的B+树。记得吗?索引都是有序的。说明一下,也有一些其余索引好比bitmap 索引,他们须要更少的成本在CPU,磁盘IO和内存,相对于B+树索引。 此外,不少现代数据库当前查询动态建立临时索引,若是这个技术可以为优化查询计划成本。
在执行join以前,你必须获得你的数据。这里就是你如何获得数据的方法。 注:全部访问路径的问题都是磁盘IO,我将不介绍太多时间复杂度的东西。
全扫描
若是已经看个一个执行计划,你必定看过一个词full scan(或者just scan)。全扫描简单的说就是数据库读整个表或者这个的索引。对磁盘IO来讲,整表扫描但是性能耗费的要比整个索引扫描多得多。
范围扫描
还有其余的扫描方式好比索引范围扫描。举一个它使用的例子,咱们使用一些像"WHERE AGE > 20 AND AGE <40"计算的时候,范围就会使用。
固然咱们在字段AGE上有索引,就会使用索引范围扫描。
咱们已经在第一章节看到这个范围查询的时间复杂度就是Log(N)+M,这个N就是索引数据。M就是一个范围内行的数目的估算。由于统计N和M都是已知(注:M就是范围计算 AGE >20 AND AGE<40的选择性)。 此外,对一个范围查询来讲,你不须要读取整个索引,因此在磁盘IO上,有比全扫描有更好的性能。
惟一扫描
你只须要索引中获得一个值,咱们称之为惟一扫描
经过rowid访问
在大部分时间里,数据库使用索引,数据库会查找关联到索引的行。经过rowid访问能够达到相同的目的。
举个例子,若是你执行
SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28
若是你有一个索引在列age上,优化器将会使用索引为你找到全部年龄在28岁的人,数据库会查找关联的行。由于索引只有age信息,而咱们想知道lastname和firstname。
可是,若是你要作这个查询
SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON WHERE PERSON.AGE = TYPE_PERSON.AGE
PERSON上的索引会用来链接TYPE_PERSON。可是PERSON将不会经过rowid进行访问。由于咱们没有获取这个表的信息。
即便这个查询在某些访问可以工做的很好,这个查询真真正的问题是磁盘IO。若是你须要经过rowid访问太多的行,数据库可能会选择全扫描。
其余方法
我不能列举全部的访问方法。若是你须要知道的更多,你能够去看[Oracle documentation]()。名字可能和其余数据库不同,可是背后的机制是同样的。
链接操做符
咱们知道如何获取咱们的数据,咱们链接他们!
我列举3个常见的链接操做:归并链接,哈希链接和嵌套循环链接。再次以前,我须要介绍几个新名词:内部关系和外部关系。一个关系(应用在):
当你链接两个关系,join运算不一样的方式管理两种关系。在剩下的文章里边,我假设:
举例, A join B 就是一个A-B链接查询,A是外部关系,B是内部关系。
一般,A join B的成本和B join A的成本是不同的。
在这部分,我假设外部关系有N个元素,内部关系有M个元素。记住,一个真正的优化器经过统计知道N和M的值。
注:N和M都是关系的基数。
这是伪代码
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)
从磁盘IO来讲,外部关系的N行数据每个行,内部循环须要读取M行数据。这个算法须要读N+N*M行数据从磁盘上。可是,若是内部关系足够小,你就能把这个关系放在内存中这样就只有M+N 次读取数据。经过这个修改,内部关系必须是最小的那个,由于这样这个算法,才能有最大的机会在内存操做。
从时间复杂度来讲,它没有任何区别,可是在磁盘IO上,这个是更好的读取方法对于二者。
固然,内部关系将会使用索引,这样对磁盘IO将会更好。
由于这个算法是很是简单,这也是对磁盘IO更好的版本,若是内部关系可以彻底存放在内存中。这就是思路:
这是可行的算法:
// 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 +cost_to_create_hash_table(M) + cost_of_hash_function*N
若是哈希函数建立足够小的域,这个复杂度为时间复杂度为O(M+N)
这就是另外一个版本的哈希链接,它更多的内存,和更少的磁盘IO。
归并链接
归并链接是惟一产生有序结果的链接
注:在这个简化的归并链接,没有内部表和外部表的区别。他们是一样的角色。可是实际实现中又一些区别。好比:处理赋值的时候。
归并链接能够分为两个步骤:
排序
咱们已经说过了归并排序,从这里来讲,归并排序是一个好的算法(若是有足够内存,还有性能更好的算法)。
可是有时数据集已是排好序的。好比:
归并链接
这一部分比起归并排序简单多了。可是此次,不须要挑选每个元素,我只须要挑选二者相等的元素。思路以下:
这样执行是由于两个关系都是排好序的,你不须要回头找元素。
这个算法是简化以后的算法。由于它没有处理两个关系中都会出现多个相同值的状况。实际的版本就是在这个状况上变得复杂了。这也是我选了一个简化的版本。
在两个关系都是排好序的状况下,时间复杂度为O(M+N) 两个关系都须要排序的状况下时间复杂度加上排序的消耗 O(N*Log(N) + M*Log(M))
对于专一于计算机的极客,这是一个处理多个匹配算法(注:这个算法我不能肯定是100%正确的)。
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 integer a_key_temp:=a_key; integer b_key_temp:=b_key; if (a[a_key+1] != b[b_key]) b_key_temp:= b_key + 1; end if if (b[b_key+1] != a[a_key]) a_key_temp:= a_key + 1; end if if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1]) a_key_temp:= a_key + 1; b_key_temp:= b_key + 1; end if a_key:= a_key_temp; b_key:= b_key_temp; end if end while
哪个是最好的链接算法
若是有一个最好的链接算法,那么它不会有这么多种链接算法。选择出一个最好的链接算法,太困难。他有那么评判标准:
你想让多进程/多线程来执行链接操做。
更多内容,请看,
,
的文档。
例子
咱们已经见过了3种链接操做。
如今若是你须要看到一我的的所有信息,要链接5张表。一我的可能有: 多个手机电话 多个邮箱 多个地址 多个银行帐户
总而言之,这么多的信息,须要一个这样的查询:
SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS WHERE SPERSON.PERSON_ID = MOBILES.PERSON_ID SAND PERSON.PERSON_ID = MAILS.PERSON_ID SAND PERSON.PERSON_ID = ADRESSES.PERSON_ID SAND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID
若是一个查询优化器,我得找到最好的方法处理这些数据。这里就有一个问题:
我该选择那种链接查询?
我有三种备选的链接查询(哈希,归并,嵌套循环),由于他们可以使用0,1,2个索引(先不提有不一样的索引)。
这里就有2种规则:
逻辑:我能够删除没有用的可能,可是不能过滤不少的可能。 好比:使用嵌套循环链接的内部关系必定是最小的数据集。
我能够接受不是最优解。使用更加有约束性的条件,减小更多的可行方法。好比:若是一个关系很小,使用嵌套循环查询,而不是归并、哈希查询。
在这个简单例子中,我获得了那么多的可行方法。可是一个现实的查询还有其余的关系操做符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT …这意味着更多更多的可行方法。
这个数据库是怎么作的呢?
我已经提到一个数据库要尝试不少种方法。真正的优化就是在必定时间内找到一个好的解。
大多数状况下,优化器找到的是一个次优解,找不到最优解。
小一点的查询,暴力破解的方式也是可行的。可是有一种方法避免了不少的重复计算。这个算法就是动态规划。
动态规划
动态规划的着眼点是有不一样的执行计划的有些步骤是同样的。若是你看这些下边的这些执行计划:
他们使用了相同的子树(A JOIN B),因此每个执行计划都会计算这个操做。在这儿,咱们计算一次,保存这个结果,等到从新计算它的时候,就能够直接用这个结果。更加正式的说,咱们遇到一些有部分重复计算的问题,为了不额外的计算,咱们用内存保存重复计算的值。
使用这个技术,咱们仅仅有了3^N的时间复杂度,而不是(2*N)!/(N+1)!。在上个例子中的4个链接操做,经过使用动态规划,备选计划从336减小到81。若是咱们使用一个更大的嗯 8链接的查询,就会从57657个选择减小到6561。
对于玩计算机的极客们,我以前看到了一个算法,在这个。提醒:我不许备在这里具体解释这个算法,若是你已经了解了动态规划或者你很擅长算法。
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查询的最优解。以后,找到每一个步骤JOIN的最优解,而后增长到执行计划。
让咱们开始这个简单的例子。好比:咱们有一个查询,有5张表的4次join操做(A, B, C, D, E)。为了简化这个问题,咱们使用嵌套循环链接。咱们使用这个准则:使用最低成本的JOIN。
由于我是随意的从A开始,固然咱们也能够指定从B,或者C,D,E 开始咱们的算法。咱们也是经过这个过程获得性能最好的执行计划。
这个算法有一个名字,叫作。
我不深刻的讲解算法的细节了。在一个良好的设计模型状况下,达到N*log(N)时间复杂度,这个问题将会被很好的解决。这个算法的时间复杂度是O(N*log(N)),对于所有动态计算的算法为O(3^N)。若是你有一个达到20个join的查询,这就意味着26 vs 3 486 784 401,这是一个天上地下的差异。
这个算法的问题在于咱们假设咱们在两张表中已经使用了最好的链接方法的选择,经过这个链接方式,已经给咱们最好的链接成本。可是:
为了优化性能,你能够运行多个贪婪算法使用不一样的规则,最后选择最好的执行计划。
其余算法
[若是你已经充分了解了这些算法,你就能够跳跃过下一部分。我想说的是:它不会影响剩下的文章的阅读]
对于不少计算机研究学者而言,得到最好的执行计划是一个很是活跃的研究领域。他们常常尝试在更加实用的场景和问题上,找到更好的解决方案。好比:
注: star join不太肯定是什么链接查询。
这些算法在大规模的查询的状况下,能够用来取代动态规划。贪婪算法属于启发式算法这一大类。贪婪算法是听从一个规则(思路),在找到当前步骤的解决方法,并将以前步骤的解决方法结合在一块儿。一些算法听从这个规则,一步一步的执行,但并不必定使用以前步骤的最优解。这个叫作启发式算法。
好比,遗传算法听从这样的规则--最后一步的最优解一般是不保留的:
越多的循环次数,会获得更好的执行计划的。
这是魔术吗?不,这是天然的规则:只有最适合的才会存在。
另外,已经实现了遗传算法,可是我还不了解是否默认使用了这个算法。
数据库使用了其余的启发式算法,好比Annealing, Iterative Improvement, Two-Phase Optimization… 可是我不了解这些算法是否用在企业数据库中,或者还在处于数据库研究的状态上。
实际的优化器
[能够跳过这一章,这一章对于本文不重要,也不影响阅读]
我已经说了这么多,并且这么理论,可是我是一个开发人员,不是一个研究人员。我更喜欢实实在在的例子
让我看一下 是如何工做的。SQLite是一个轻量级的数据库,它使用了很是简单优化方法,具体是基于贪婪算法,添加额外的约束,以减小可选解数量。
好吧,让我了解一下其余优化怎么来工做。IBM DB2像其余的企业级数据库同样,它是我关注大数据以前专一的最后一个数据库。
若是咱们查看了DB2的官方文档,咱们会了解到DB2的优化器有7个优化层次。
咱们能够了解到DB2使用了贪婪算法。固然,自从查询优化器成为数据库的一大动力的时候,他们再也不分享他们使用的启发式算法。
说一句,默认的优化层次是5。缺省状况下,优化器使用如下数据:
默认状况下, DB2 在选择链接顺序时候,使用启发算法约束的动态规划
其余的SQL选择条件能够使用简单的规则
查询计划缓存
建立一个执行计划是须要时间的,大多数的数据库都把这些查询计划存储在查询计划缓存中,减小从新计算这些相同的查询计划的消耗。只是一个很是大的课题,由于数据库必须知道何时替换掉已通过期无用的计划。这个方法是建立一个阀值,当统计信息中表结构产生了变化,高于这个阀值,数据库就必须将涉及这张表的查询计划删除,净化缓存。
辛辛苦苦到了这个环节,咱们已经得到优化过的执行计划。这个计划已是编译成了可执行代码。若是有了足够的资源(内存,CPU),查询执行器就会执行这个 计划。计划中的操做(JOIN, SORT BY....)能够顺序执行,也能够并行执行,全看执行器。为了获取和写入数据,执行器必须和数据管理器打交道,也就是下一节的内容。
到了这一步,查询管理器执行查询,须要从表,索引获取数据。它从数据管理器请求数据,有2个问题:
在这个部分,咱们将会看到关系型数据库如何处理这2个问题。咱们不会讨论管理器如何获取数据,由于这个不是很是重要(本文如今也太长了)。
我已经说过,数据库最大的瓶颈就是磁盘I/O。为了提升性能,现代数据库使用缓存管理器。
相比于直接从文件系统获取数据,查询执行器从缓存管理器中请求数据。缓存管理器有一个常驻内存的缓存叫作内存池,直接从内存获取数据可以极大极大的提升数据库的性能。可是很难说明性能提高的量级,由于这个跟你的执行操做息息相关。
可是内存要比磁盘操做快100到10W倍
这个速度的差别引出了另外一个问题(数据库也有这个问题)。缓存管理器须要在查询执行器使用以前加载数据到内存,否则查询管理器必须等待从慢腾腾磁盘上获取数据。
这个问题叫作预加载,查询执行器知道它须要那些数据。由于它已经知道了查询的整个流程,经过统计数据已经了解磁盘上的数据。这里有一个加载思路:
缓存管理器在内存池中存储全部这些数据。为了肯定数据须要与否,管理器附加缓存中的数据额外的管理信息(称为latch)。 注:latch真的无法翻译了。
有时,查询执行器不知道他须要什么样的数据,由于数据库并不提供这项功能。相应的,数据使用推测性预加载(好比:若是查询执行器要求数据1,3,5,他们他极可能继续请求7,9,11)或者连续预加载(这个例子中,缓存管理器从磁盘上顺序加载数据,根据执行器的请求)
为了显示预加载的工做状况,现代数据提供一个衡量参数叫作buffer/cache hit ratio请求数据在缓存中几率。
注:很低的缓存命中率不意味着缓存工做的很差。更多的信息能够参考。
若是缓存是一个很是少的内存。那么,他就须要清除一部分数据,才能加载新的数据。数据的加载和清除,都须要消耗成本在磁盘和网络I/O上。若是一个查询常常执行,频繁的加载/清除数据对于这个查询也不是很是有效率的。为了解决这个问题,现代数据库使用内存更新策略。
内存更新策略
如今数据库(SQL Server,MySQL,Oracle和DB2)使用LRU算法。
LUR
LRU全称是Least Recently Used,近期最少使用算法。算法思路是最近使用的数据应该驻留在缓存,由于他们是最有可能继续使用的数据。
这是可视化的例子:
综合考虑,咱们假设缓存中的数据没有被latches锁定(这样能够被清除)。这个简单的例子中,缓存能够存储3个元素:
这个算法工做的很好,可是有一些限制。若是在一个大表上进行全扫描,怎么办?话句话说,若是表/索引的大小超过的内存的大小,怎么办?使用这个算法就会移除缓存中以前的全部数据,然而全扫描的数据只使用一次。
加强方法
为了不这个这个问题,一些数据增长了一些规则。好比 Oracle请看
“For very large tables, the database typically uses a direct path read, which loads blocks directly […], to avoid populating the buffer cache. For medium size tables, the database may use a direct read or a cache read. If it decides to use a cache read, then the database places the blocks at the end of the LRU list to prevent the scan from effectively cleaning out the buffer cache.” (对于表很是大的状况,数据典型处理方法,直接地址访问,直接加载磁盘的数据块,减小填充缓存的环节。对于中型的表,数据块使用直接读地盘,或者读缓存。 若是选择了读缓存,数据库将数据块放在LRU列表的最后,防止扫描将这个数据块清除)
如今有更多的选择了,好比LRU的新版本,叫作LRU-K,在SQL-Server中使用了LRU-K,K=2.
算法的思路是记录更多的历史信息。对于简单的LRU(也能够认为是LRU-K,K=1),算法仅仅记录了最后一个数据使用的信息。对于LRU-K
权重的计算是很是耗费成本的,因此SQL-Server仅仅使用了K=2.整体来看,使用这个值,运行状况也是能够接受的。
关于LRU-K,更多更深刻的信息信息,你能够阅读研究文档(1993):
其余算法
固然,管理缓存许多其余的算法好比:
一些数据库提供了使用其余算法而不是默认算法的方法。
写缓存
我仅仅讨论了读缓存--使用数据以前,载入数据。可是数据库中,你还必须有写缓存,这样你能够一次批量的写入磁盘。而不是写一次数据就写一次磁盘,减小单次的磁盘访问。
记住缓存保存数据页(Pages,数据的最小单元)而不是行(这是人/逻辑看待数据的方式)。若是一个页被修改而没有写入磁盘,那么缓存池中的这个数据页是脏的。选择写入磁盘时间的算法有不少,可是他们都和事务的概念息息相关。这就是本文下一章要讲述的。
本文最后,也就是本章内容就是事务管理器。咱们将看到一个进程如何保证每个查询都是在本身的事务中执行的。可是在此以前,咱们必须了解事务 ACID的概念。
I‘m on acid
事务是关系型数据库的工做单元,它有四个性质:
在一个事务里边,你能够有不少SQL语句去对数据增删改查。混乱的开始:两个事务同时使用相同的数据。典型例子:一个转帐从帐户A到帐户B。想象一下,你有两个事务:
若是咱们回到事务的ACID性质:
好比,若是一个事务 A执行"select count(1) from TABEL_X",在这个时候,事务B向TABEL_X中,增长了一条新的数据,并正确提交,若是事务A 从新执行count(1),得到的值是不一样的。
这叫作幻读。
这个叫作脏读。
大部分数据使用它本身独特的隔离级别(就像Post供热SQL, Oracle,SQL Server使用的快照隔离)。而后,更多数据一般并非所有的SQL规范的四个隔离,尤为是读未提交。
默认的隔离级别能够在数据库链接开始的时候(仅仅须要添加很是简单的代码),被用户和开发者从新定义。
隔离,一致性和原子性的真正问题是在相同数据上的写操做(增长,修改和删除):
这个问题叫作并行控制
解决这个问题最简单的方式是一个接一个执行事务(好比:串行)。可是它不能进行扩展,即便运行在多核多处理的服务器上也只能使用一个核。很是没有效率...
解决这个问题的理想方式是:在每一时刻,事务均可以常见或者取消:
更加正式的说,这是一个冲突调度时候的再调度问题。更加具体的说,这个是一个困难的,CPU密集型的优化问题。企业型数据库确定不能耗费数小时去给每一个事务去找到到最好的调度方式。所以,他们使用次于理想方式的途径,这些方式致使处理冲突的事务更多的时间浪费。
为了解决这个问题,大部分数据库使用锁而且/或者数据版本。这个是一个大的话题,我将关注于锁。以后我会讲解写数据版本。
悲观锁
锁机制的思路是:
这就被称为独占锁
可是事务在只须要读数据的时候,却使用独占锁就太浪费了。由于它强制其余事务在读相同的数据的时候也必须等待。这就是为何须要另外一种锁,共享锁。
共享锁的思路:
另外,若是数据被施加独占锁,一个事务只须要读这个数据,也必须等待独占锁的结束,而后对数据加共享锁。
锁管理器是加锁和解锁的过程。从实现上来讲,它在哈希表中存储着锁(键值是要锁的数据),以及对应的数据。
死锁
可是锁的使用会致使一个问题,两个事务在永远的等待对方锁定的数据。
在这个例子中:
这就是死锁
在死锁中,锁管理器为了不死锁,会选择取消(回滚)其中一个事务。这个选择不容易的:
在咱们作出选择以前,必须肯定是否存在死锁。
这个哈希表,能够被看作是图(像以前的例子)。若是在图中产生了一个循环,就有一个死锁。由于确认环太浪费性能(由于有环的图通常都很是大),这里有一个经常使用小技术:使用超时。若是一个锁没有在超时的时间内结束,这个事务就进入了死锁状态。
锁管理器在加锁以前也会检测这个锁会不会产生死锁。可是重复一下,作这个检测是很是耗费性能的。所以,这些提早的检测是基本的规则。
两段锁
营造纯净的隔离最简单的方式是在事务开始的时候加锁,在事务结束的时候解锁。这意味着事务开始必须等待全部的锁,在事务结束的时候必须解除它拥有的锁。这是能够工做的可是产生了巨大的时间浪费。
一个更快速的方法是两段锁协议(DB2,SQL Server使用这项技术),在这项技术中,事务被划分红两个极端:
这个协议工做的很好,除了这个状况,即一个事务修改数据,在释放关联的锁被取消(回滚)。这个状况结束时候,会致使其余事务读取修改后的值,而这个值就要回滚。为了解决这个问题,全部的独占锁必须在事务完成的时候释放。
一些话
固然真正的数据库是更加复杂、精细的系统,使用了更多类型锁(好比意向锁),更多的控制粒度(能够锁定一行,锁定一页,锁定一个分区,锁定一张表,锁定表分区)。可是基本思路是同样的。
我只讲述了段春基于锁的方法。数据版本是另外一个处理这个问题的方式
版本处理的思路是:
这个种方式提升了性能:
万物皆美好啊,除了两个事务同时写一份数据。所以,你在结束时候,会有巨大磁盘空间浪费。
数据版本和锁是两个不一样的方式:乐观锁和悲观锁。他们都有正反两方面,根据你的使用状况(读的多仍是写的多)。对于数据版本的介绍,我推荐这个关于Post供热SQL实现多版本的并发控制是
一些数据库好比DB2(一直到DB2 9.7),SQL Server(除了快照隔离)都只是使用了锁。其余的像PostgreSQL,MySQL和Oracle是使用了混合的方式包括锁,数据版本。我还不知道 有什么数据库只使用了数据版本(若是你知道那个数据库只是用了数据版本,请告诉我)。
[更新在2015/08/20],一个读者告诉我:
Firebird和 Interbase就是只使用了数据版本,没有使用记录锁,版本控制对于索引来讲也是有很是有意思的影响:有时,一个惟一索引包含了副本,索引的数目比数据的行更多
若是你读到关于不一样的隔离层次的时候,你能够增长隔离层次,你增长锁的数量,所以事务在等待锁时间的浪费。这就是不少数据库默认不使用最高级别隔离(串行化)的缘由。
固然,你能够本身查看这些主流数据库的文档(像 Mysql,PostgreSQL,Oracle)。
咱们已经看到为了增长性能,数据库在内存中存储数据。事务已经提交,可是一旦服务器崩溃,你但是可能丢失在内存中的数据,这就破坏了事务的持久性。
若是服务器崩溃的时候,你正在向磁盘写入数据。你会获得一部分写入磁盘的结果,这样就破坏了事务的原子性。
事务的任何修改写入都是要不不作要么作完
为了解决这个问题,有两个方法:
WAL
当大型数据库上许多事务都在运行,影子拷贝/影子页有一个巨大的磁盘使用过量。这就是现代数据库使用事务日志。事务日志必须存储在稳定的存储介质上,我不能更加深刻的挖掘存储技术可是使用(起码)RAID 磁盘是必须,避免磁盘损坏。
大部分现代数据库(起码Oracle,SQL Server, DB2, PostgreSQL,Mysql和SQLite)处理事务日志使用了*Write-Ahead Logging protocol *(WAL),这个WAL协议是一组规则:
这个是日志管理器的工做。在缓存管理器和数据访问管理器(它将数据吸入磁盘)中间,就能找到它的身影,日志管理器把每一个update/delete/create/commit/rollback,在写入磁盘以前,将相应的信息在事务日志上。很简单,不是吗?
错误的答案!毕竟咱们已经想过,我已经知道和数据库相关的一切事情都被”database effect“所诅咒。更加严重的问题是,找到具备很好性能的日志写入方法。若是吸入日志太慢,他们竟会拖慢全部的事情。
ARIES
在1992年,IBM的研究人员”发明“了一个WAL的加强版本叫作ARIES。ARIES差很少已经被全部的现代数据使用。虽然逻辑处理有一些差别,可是ARIES的思想都已经遍地开花了。跟着, 我也引用了这项发明的思想,”没有比写一个好的事务恢复更好的作法“。在我5岁的时候,ARIES论文已经发表,我不了解那些苦逼的研究人民的传言。我打 算在咱们开始最后的技术章节以前,将一些有意思的东西,放松一下。我阅读了ARIES很大篇幅的研究论文,我发现这个颇有意思。我想讲述一个ARIES的 总体的形态,可是若是你有一些真材实料,强烈推荐去阅读那篇论文。
ARIES全称是Algorithms for Recovery and Isolation Exploiting Semantics。
这项技术的两个目的:
数据库回滚事务有多种缘由:
有时候(好比,遇到网络失败),数据库能恢复事务。
可是这可能吗?为了回答这个问题,咱们必须明白信息就在日志记录里面。
日志
每个事务的每个操做(dadd/remove/modify)都要产生日志。日志记录包括:
好比,一个Update操做,这个UNDO就要存放update以前,要update元素的元素值(物理UNDO)或者能够回归以前状态的逆运算(物理UNDO)。
注:原文是Page,可是磁盘单位可是咱们更愿意用块。
此外,磁盘的每一个块(存储数据,不是日志)都有最有一个最后修改数据的操做日志记录的ID(LSN)
*这个方式LSN更复杂,由于由于他还要牵涉到日志存储。可是思路是同样的。
**ARIES只只用逻辑UNDO,由于处理物理UNDO才是个大麻烦。
注:从我这点浅薄的看法,只有PostgreSQL没有使用UNDO。它使用一个垃圾收集服务,有这个服务来清除老版本的数据。这个实现跟PostgreSQL的数据版本时间有关。
为了更清楚的理解,这个一个简化的图形,这个例子说明的是语句”UPDATE FROM PERSON SET AGE = 18;“产生的日志记录。这个语句是在ID18的事务中执行的。
每个日志都有惟一的LSN。这些日志经过相同的事务关联在一块儿,经过时间顺序进行逻辑管理。(这个执行链的最后一条日志,也是以后一个操做的日志)
日志缓冲
为了不日志写入成为性能瓶颈,引入了日志缓冲。
若是查询执行器要求这样的修改:
当一个事务已经提交,这意味着事务中的每个操做的1,2,3,4,5个步骤都已经完成。写入事务日志挺快的,由于它仅仅是”在事务日志中增长记录“,然而写入数据是很是复杂的,由于”要用方便、快速读的方式写入数据“。
STEAL和FORCE模式
性能缘由,5个步骤可能在提交以后才能完成,由于在崩溃的状况下,可能须要REDO日志恢复事务执行。这是就是NO-FORCE policy(非强制模式)
一个数据库能够选择强制模式(FORCE policy)(五个步骤必须在提交以前完成),这样能够减小恢复过程当中的负载。
另外一个问题是选择一步一步将数据写入磁盘(STEAL Policy),仍是缓存管理器等待,直到提交指令,一次将全部的修改一次性写入磁盘(NO-STEAL)。在STEAL和NO STEAL中进行选择,要看你的须要。快速写入可是使用UNDO日志数据恢复慢,仍是快速恢复。
不一样的模式对于数据恢复有不一样的影响,请看以下总结:
注: STEAL/NO STEAL描述对象是修改的数据
FORCE/NO FORCE说明的对象是写入日志的时间。
数据恢复
OK,咱们已经有了很是好的日志,让咱们来使用它。
假设数据由于内部错误而崩溃,你重启数据库,这个恢复程序就开始了。
ARIES从失败中经过三个方法进行恢复
在恢复过程当中,事务日志必须提醒恢复程序,他们要作出的行动,由于数据写入磁盘和写入事务日志是同步的。一个解决方案能够删除未完成的事务的条目,可是至关困难。相应的,ARIES在事务日志中写入综合性的日志,能够逻辑删除那些已经移除的事务的日志条目。
当一个事务被”手动的“取消,或者被锁管理器(为了解决死锁),或者仅仅由于网络失败,这个时候分析方法就是不须要的。事实上,那些须要REDO,那些须要UNDO的信息是在两个内存中的表里边:
这些表被缓存管理器更新,在新事务建立时候,事务管理器更新。由于他们是在内存中的,当数据库崩溃,他们也要被销毁。
分析阶段的工做就是运用事务日志的信息,重建崩溃后的两张表。为了加快分析速度,ARIES提供了检查点(checkpoint)*的概念。这个思路就是将事务表,脏页表一次一次的写入磁盘,在写入磁盘的时候,保存的最后一个LSN也写入磁盘。在分析阶段,以后LSN以后的日志才会被分析。