可以看到上图将数据库分为四个模块,分别是核心组件,查询管理器,数据管理器和工具类
核心组件
安全管理器 用于对用户的验证和授权。
客户端管理器 用于管理客户端连接
内存管理器 为了避免磁盘I/O带来的性能损失,需要大量的内存。但是如果你要处理大容量内存你需要高效的内存管理器,尤其是你有很多查询同时使用内存的时候。
文件系统管理器 磁盘I/O是数据库的首要瓶颈。具备一个文件系统管理器来完美地处理OS文件系统甚至取代OS文件系统,是非常重要的。
网络管理器 网路I/O是个大问题,尤其是对于分布式数据库。所以一些数据库具备自己的网络管理器。
进程管理器 很多数据库具备一个需要妥善管理的进程/线程池。再者,为了实现纳秒级操作,一些现代数据库使用自己的线程而不是操作系统线程。
工具
备份管理器 用于保存和恢复数据复原管理器 用于崩溃后重启数据库到一个一致状态监控管理器 用于记录数据库活动信息和提供监控数据库的工具。Administration管理器 用于保存元数据(比如表的名称和结构),提供管理数据库、模式、表空间的工具查询管理器
查询解析器 检查查询是否合法
查询重写器 预优化查询
查询优化器 优化查询
查询执行器 编译和执行查询
数据管理器
事务管理器用于处理事务
缓存管理器 数据被使用之前置于内存,或者数据写入磁盘之前置于内存
数据访问管理器 访问磁盘中的数据
客户端管理器是处理客户端通信的。客户端可以是一个(网站)服务器或者一个最终用户或最终应用。客户端管理器通过一系列知名的API(JDBC, ODBC, OLE-DB …)提供不同的方式来访问数据库。
客户端管理器也提供专有的数据库访问API。
当连接到数据库时:
管理器首先检查你的验证信息(用户名和密码),然后检查你是否有访问数据库的授权。这些权限由DBA分配。
然后,管理器检查是否有空闲进程(或线程)来处理你对查询。
管理器还会检查数据库是否负载很重。
管理器可能会等待一会儿来获取需要的资源。如果等待时间达到超时时间,它会关闭连接并给出一个可读的错误信息。
然后管理器会把你的查询送给查询管理器来处理。
因为查询处理进程不是『不全则无』的,一旦它从查询管理器得到数据,它会把部分结果保存到一个缓冲区并且开始给你发送。
如果遇到问题,管理器关闭连接,向你发送可读的解释信息,然后释放资源。
操作过程:
查询首先被解析并判断是否合法然后被重写,去除了无用的操作并且加入预优化部分接着被优化以便提升性能,并被转换为可执行代码和数据访问计划。然后计划被编译最后,被执行功能: 1. 检查语法和关键字顺序
SELECT 写成 SLECT WHERE 写在 SELECT 之前2. 检查表和字段
1.表是否存在 2.表的字段是否存在 3.对某类型字段的 运算 是否 可能(比如,你不能将整数和字符串进行比较,你不能对一个整数使用 substring() 函数)3. 检查在查询中是否有权限来读写数据
在解析过程中SQL 查询被转换为内部表示(通常是一个树) 如果一些解析正常 内部会把结果送到查询重写器
目标:
预优化查询避免不必要的运算帮助优化器找到合理的解决方案基本操作
视图合并 如果你在查询中使用视图,视图就会转换为它的 SQL 代码
子查询扁平化 子查询是很难优化的,因此重写器会尝试移除子查询
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%';3.去除不必要的运算符 比如,如果你用了 DISTINCT,而其实你有 UNIQUE 约束(这本身就防止了数据出现重复),那么 DISTINCT 关键字就被去掉了
4.排除冗余的联接 如果相同的 JOIN 条件出现两次,比如隐藏在视图中的 JOIN 条件,或者由于传递性产生的无用 JOIN,都会被消除。
5.常数计算赋值 如果你的查询需要计算,那么在重写过程中计算会执行一次。比如 WHERE AGE > 10+2 会转换为 WHERE AGE > 12 , TODATE(“日期字符串”) 会转换为 datetime 格式的日期值
6.(高级)分区裁剪(Partition Pruning) 如果你用了分区表,重写器能够找到需要使用的分区。
7.(高级)物化视图重写(Materialized view rewrite) 如果你有个物化视图匹配查询谓词的一个子集,重写器将检查视图是否最新并修改查询,令查询使用物化视图而不是原始表。
8.(高级)自定义规则 如果你有自定义规则来修改查询(就像 Oracle policy),重写器就会执行这些规则。
9.(高级)OLAP转换 分析/加窗 函数,星形联接,ROLLUP 函数……都会发生转换(但我不确定这是由重写器还是优化器来完成,因为两个进程联系很紧,必须看是什么数据库)。
当你要求数据库收集统计信息,数据库会计算下列值
表中行和页的数量表中每个列中的: 唯一值数据长度(最小,最大,平均)数据范围(最小,最大,平均)表的索引信息这些统计信息会帮助优化器估计查询所需的磁盘 I/O、CPU、和内存使用
在研究 B+树的时候我们谈到了索引,要记住一点,索引都是已经排了序的。
仅供参考:还有其他类型的索引,比如位图索引,在 CPU、磁盘I/O、和内存方面与B+树索引的成本并不相同。
另外,很多现代数据库为了改善执行计划的成本,可以仅为当前查询动态地生成临时索引。
在应用联接运算符(join operators)之前,你首先需要获得数据。以下就是获得数据的方法。
全扫描 数据库完整的读一个表或索引范围扫描 其他类型的扫描有索引范围扫描,比如当你使用谓词 ” WHERE AGE > 20 AND AGE < 40 ” 的时候它就会发生。唯一扫描 如果你只需要从索引中取一个值你可以用唯一扫描根据ROW ID存取A join B:称A是外联系,B是内联系,N是外联系中元素个数,M是内联系中元素个数
嵌套循环连接原理如下 1. 针对外关系的每一行 2. 查看内关系里的所有行来寻找匹配的行
哈希连接原理: 1) 读取内关系的所有元素 2) 在内存里建一个哈希表 3) 逐条读取外关系的所有元素 4) (用哈希表的哈希函数)计算每个元素的哈希值,来查找内关系里相关的哈希桶内 5) 是否与外关系的元素匹配。
合并连接原理 1.(可选)排序联接运算:两个输入源都按照联接关键字排序。[归并排序] 2.合并联接运算:排序后的输入源合并到一起。
三种连接算法的比较
空闲内存 没有足够的内存就使用hash连接两个数据集的大小: 如果一个大表连接一个很小的表 嵌套循环连接就比hash连接块很多 如果都很大 那么嵌套循环的CPU成本就很高是否有索引 有两个B+树索引的话 合并连接的效率最高是否需要排序是否已经排序数据的分布 如果数据是倾斜的 那么不适合hash函数在这个阶段,我们有了一个优化的执行计划,再编译为可执行代码。然后,如果有足够资源(内存,CPU),查询执行器就会执行它。计划中的操作符 (JOIN, SORT BY …) 可以顺序或并行执行,这取决于执行器。为了获得和写入数据,查询执行器与数据管理器交互,本文下一部分来讨论数据管理器。
查询管理器执行了查询,需要从表和索引获取数据,于是向数据管理器提出请求
但是有 2 个问题:
关系型数据库使用事务模型,所以,当其他人在同一时刻使用或修改数据时,你无法得到这部分数据。数据提取是数据库中速度最慢的操作,所以数据管理器需要足够聪明地获得数据并保存在内存缓冲区内。查询执行器不会直接从文件系统拿数据,而是向缓存管理器要。缓存管理器有一个内存缓存区,叫做缓冲池,从内存读取数据显著地提升数据库性能。
缓存管理器需要在查询执行器使用数据之前得到数据,否则查询管理器不得不等待数据从缓慢的磁盘中读出来。
有时查询执行器不知道它需要什么数据,有的数据库也不提供这个功能。相反,它们使用一种推测预读法(比如:如果查询执行器想要数据1、3、5,它不久后很可能会要 7、9、11),或者顺序预读法(这时候缓存管理器只是读取一批数据后简单地从磁盘加载下一批连续数据)。
LRU 最近最少使用(Least Recently Used)算法
之前探讨的都是读缓存 —— 在使用之前预先加载数据。用来保存数据、成批刷入磁盘,而不是逐条写入数据从而造成很多单次磁盘访问
要记住,缓冲区保存的是页(最小的数据单位)而不是行(逻辑上/人类习惯的观察数据的方式)。缓冲池内的页如果被修改了但还没有写入磁盘,就是脏页。有很多算法来决定写入脏页的最佳时机,但这个问题与事务的概念高度关联,下面我们就谈谈事务。
原子性(Atomicity): 事务『要么全部完成,要么全部取消』,即使它持续运行10个小时。如果事务崩溃,状态回到事务之前(事务回滚)。
隔离性(Isolation): 如果2个事务 A 和 B 同时运行,事务 A 和 B 最终的结果是相同的,不管 A 是结束于 B 之前/之后/运行期间。
持久性(Durability): 一旦事务提交(也就是成功执行),不管发生什么(崩溃或者出错),数据要保存在数据库中。
一致性(Consistency): 只有合法的数据(依照关系约束和函数约束)能写入数据库,一致性与原子性和隔离性有关。
事务的隔离级别并不是越高越好,越高的事务隔离级别会增加锁的数量和事务等待锁的时间
多个事务在相同时刻对相同数据进行读写操作 理想的方法是
监控所有事务的所有操作检查是否2个(或更多)事务的部分操作因为读取/修改相同的数据而存在冲突重新编排冲突事务中的操作来减少冲突的部分按照一定的顺序执行冲突的部分(同时非冲突事务仍然在并发运行)考虑事务有可能被取消悲观锁
原理是:
如果一个事务需要一条数据它就把数据锁住如果另一个事务也需要这条数据它就必须要等第一个事务释放这条数据这个锁叫排他锁。
但是对一个仅仅读取数据的事务使用排他锁非常昂贵,因为这会迫使其它只需要读取相同数据的事务等待。因此就有了另一种锁,共享锁。
共享锁
原理:
如果一个事务只需要读取数据A它会给数据A加上『共享锁』并读取如果第二个事务也需要仅仅读取数据A它会给数据A加上『共享锁』并读取如果第三个事务需要修改数据A它会给数据A加上『排他锁』,但是必须等待另外两个事务释放它们的共享锁。锁管理器是添加和释放锁的进程,在内部用一个哈希表保存锁信息(关键字是被锁的数据),并且了解每一块数据是:
被哪个事务加的锁哪个事务在等待数据解锁死锁 2个事务永远在等待一块数据
解决方法: 在死锁发生时,锁管理器要选择取消(回滚)一个事务,以便消除死锁。
杀死数据修改量最少的事务杀死持续时间最短的事务杀死能用更少时间结束的事务我们已经知道,为了提升性能,数据库把数据保存在内存缓冲区内。但如果当事务提交时服务器崩溃,崩溃时还在内存里的数据会丢失,这破坏了事务的持久性。
你可以把所有数据都写在磁盘上,但是如果服务器崩溃,最终数据可能只有部分写入磁盘,这破坏了事务的原子性。
事务作出的任何修改必须是或者撤销,或者完成。
有 2 个办法解决这个问题:
影子副本/页(Shadow copies/pages):每个事务创建自己的数据库副本(或部分数据库的副本),并基于这个副本来工作。一旦出错,这个副本就被移除;一旦成功,数据库立即使用文件系统的一个把戏,把副本替换到数据中,然后删掉『旧』数据。事务日志(Transaction log):事务日志是一个存储空间,在每次写盘之前,数据库在事务日志中写入一些信息,这样当事务崩溃或回滚,数据库知道如何移除或完成尚未完成的事务。1) 每个对数据库的修改都产生一条日志记录,在数据写入磁盘之前日志记录必须写入事务日志。
2) 日志记录必须按顺序写入;记录 A 发生在记录 B 之前,则 A 必须写在 B 之前。
3) 当一个事务提交时,在事务成功之前,提交顺序必须写入到事务日志。
如果事务日志写得太慢,整体都会慢下来
目标 1) 写日志的同时保持良好性能 2) 快速和可靠的数据恢复
先了解日志里保存的内容
事务的每一个操作(增/删/改)产生一条日志,由如下内容组成:
LSN:一个唯一的日志序列号(Log Sequence Number)。LSN是按时间顺序分配的 * ,这意味着如果操作 A 先于操作 B,log A 的 LSN 要比 log B 的 LSN 小。TransID:产生操作的事务ID。PageID:被修改的数据在磁盘上的位置。磁盘数据的最小单位是页,所以数据的位置就是它所处页的位置。PrevLSN:同一个事务产生的上一条日志记录的链接。UNDO:取消本次操作的方法。 比如,如果操作是一次更新,UNDO将或者保存元素更新前的值/状态(物理UNDO),或者回到原来状态的反向操作(逻辑UNDO) **。REDO:重复本次操作的方法。 同样的,有 2 种方法:或者保存操作后的元素值/状态,或者保存操作本身以便重复。日志缓冲区
当查询执行器要求做一次修改:
1) 缓存管理器将修改存入自己的缓冲区; 2) 日志管理器将相关的日志存入自己的缓冲区; 3) 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改); 4) 接着(不久以后)日志管理器把日志写入事务日志,什么时候写日志由某算法来决定。 5) 接着(不久以后)缓存管理器把修改写入磁盘,什么时候写盘由某算法来决定。
ARIES从崩溃中恢复有三个阶段:
1) 分析阶段:恢复进程读取全部事务日志,来重建崩溃过程中所发生事情的时间线,决定哪个事务要回滚(所有未提交的事务都要回滚)、崩溃时哪些数据需要写盘。2) Redo阶段:这一关从分析中选中的一条日志记录开始,使用 REDO 来将数据库恢复到崩溃之前的状态。3) Undo阶段:这一阶段回滚所有崩溃时未完成的事务。回滚从每个事务的最后一条日志开始,并且按照时间倒序处理UNDO日志(使用日志记录的PrevLSN)。