昨天俱乐部内部办了一个讲座,关于常规数据库系统实现,听了之后有点混乱,于是花了很多时间特地查了一些资料,基本上自己感觉自己是明白了。特地写下来。

文章开头说明三点,

  • 第一点,本文针对常规数据库,是为了区别空间数据库;
  • 第二点,本文不追求解释清楚各个细节,而是着重介绍整体的脉络,说白了就是,本文不细究怎么做,而是探讨为什么要这么做。举个例子,我会去探讨为什么要设计B树,而不会去说明B树是怎么操作的;
  • 第三点,设计数据库存储结构的目的在于简化和协助对数据的访问。

  1.  概述

  计算机内部存储是分层次的,简单来说有内存和磁盘,内存空间小但是运算快,而磁盘空间大但是很慢。数据库系统可能很大,有几百万条记录,这样一来数据库就必须存储在磁盘里面了,每次需要访问数据库里面的数据的时候我们就需要通过I/0接口访问磁盘,然后将数据读出来。我们要理解一点,就是说从I/O接口读取数据是很慢很慢的,我们要尽量减少读取次数。往往我们读取的数据是由局部性的,所以一般每次中磁盘里面读取数据都是一下子读取一块数据而不仅仅是我们需要的那一条数据,这样做是因为我们以后要访问的数据很可能就在当前访问数据的附近,所以干脆全读出来以减少读取次数。那个“块”我们称之为磁盘块,每个磁盘块是由几个扇区组成的,一般4K左右吧(我就不继续解释扇区的概念啦)。

好了,我们的目标就在于加速对数据的访问。我们前面提到,加速访问的关键就在于怎么高效读取数据,这有两个方面。第一,我们可以想办法加速读取I/O的速度;第二,我们要想办法减少读取I/O的次数。

  2. 加速I/O读取速度

这里我不去深入到物理结构那个层次。

前面有提到现在的数据库可能很大,这就需要用很多磁盘来存储,但是我们不能天真的以为磁盘就不会坏掉,所以我们首先需要确保数据的可靠性。毕竟,如果数据都损坏了,还谈何快速访问呢?

确保可靠性的直观想法就是备份(哈哈,插一句,记得很多同学都互嘲学计算机的孩子都养成了随手备份的“恶习”)。这样一来,一个逻辑上的磁盘是由两个物理磁盘所构成的,并且每个写操作都需要在两个磁盘上执行。这样做的好处就在于,除非两个磁盘同时坏掉,否则我们的数据就是安全的,显然我们提高了可靠性。

现在回到我们一开始说的怎么加速I/O读取的速度这个问题。既然数据有备份,那么我们读取数据的时候为什么不可以同时也从备份数据中读取呢,这样,速度势必翻倍。这也就是通过并行来提升性能,涉及到数据拆分了。具体怎么做一般有比特级拆分和块级拆分两种常见办法。

有关于加速的话题就暂且谈到这里了,具体细节大家可以继续研究 冗余独立磁盘阵列(RAID)。总结一下就是通过冗余(备份)来提高可靠性,通过并行提高性能

3. 减少I/O的访问次数

花开两朵各表一枝。下面就要进入最核心的部分了,也就是怎么设计以减少I/O的访问次数。

为了说明清楚这个问题,我们必须知道记录在文件中是怎么组织起来的,因为关系就是记录的集合嘛。常见的记录组织有三种方式,堆文件组织,顺序文件组织以及散列文件组织(就不说明散列啦,本文)。

  3.1 堆文件组织

对于堆文件组织,记录是按照它们到达的顺序存放的,也就是说没有什么结构只能顺序访问了。对于这种结构,显然效率是个问题,因为如果我要查询包含某个键值的记录,我就需要顺序的读取(注意,这里的顺序是指按照记录存储的先后顺序)把整个关系表读取出来,线性查找,很麻烦吧。而且删除的时候我们需要先线性查找到那个记录,然后标记删除再写回磁盘。注意一点被删除的那个磁盘空间是不会再被使用的,这点有点影响效率。当然了,也不是一无是处,插入还是很方便的,直接插到末尾就好了,这个性质很适合批量加载数据。

  3.2 顺序文件组织

如果记录之间是有顺序的(记录根据“搜索码”排序),那么这个时候我们需要查询某个键值的时候就可以很快找到了。搜索码一般是主键,当然了,也可以不是。为了减少顺序文件处理中的I/O访问次数,我们在物理上尽可能按照搜索码顺序存储记录。插入的时候,先搜索到处于插入记录之前的那个记录,然后如果这个记录所在的块还有空间那就直接插入,否则就需要将该记录插入到新的“溢出块”中了。无论哪种情况,都需要调整指针,使得可以按照搜索码顺序吧记录链接在一起。删除的时候,也是先找到记录,然后标记删除,同时用将所有被删除的记录串起来形成“空闲列表”以便与空间管理。下面着重说一下怎么查找。(这里假定是顺序索引)

首先我们知道既然记录是顺序排列的,那么就可以二分查找了。这里面有个前提,大家知道二分查找必须要求记录是可以任意访问的,也就是说我们必须满足记录在物理空间上连续才可以二分,也就是说如果出现上面提到的“溢出块”就不能二分了,乖乖线性查找吧。我们先不考虑“溢出块”的情况

即便二分查找,那么我们查找一次的需要访问I/O log(b) 次,其中b是指所有记录存储所占用的磁盘块个数。也许你会很庆幸效率很不错嘛,但是我们来算一下,假设一个磁盘块(假设4KB)可以存储10条记录,那么十万条记录就要占用一万个磁盘块,也就是需要访问I/O十四次,一次假设30ms,那也是四百多毫秒了,是不是很恐怖!

  3.2.1 建立索引

我们回头来看刚刚的这种做法,我们等于是读取了整个记录表然后再二分查找,那比如说我只想找到学号为1111的那个学生,我其实只需要读取全部的学号,然后二分查找就可以了,至于这个学生的其他信息暂时用不到啊!所以,我们建立了附加结构--索引。索引包括搜索码和指向该记录的指针,指针包括了记录所在磁盘块的标识以及块内偏移。很显然,索引是比记录要小的多的。我们再计算一下,假设一个磁盘块可以存储100个索引记录,那么也就是需要占用1000个磁盘块,二分查找需要十次访问I/O,效率好多了吧,当然了三百毫秒也还是太长!(这里假设每个记录都有独特的所以码)

  我们接着来分析。刚刚建立的索引是为每个搜索码都建立对应的索引,也就是稠密索引。实际上我们大可不必如此,我们只需要为某些搜索码建立索引(称之为稀疏索引),然后再顺序查找就好了。我们知道,相对于I/O访问操作,我们将磁盘块数据放进内存后对其扫描的时间是可以忽略的,所以我们一般为每个磁盘块建立索引。再回到刚刚的计算,十万个记录需要占用一万个磁盘块,也就是需要一万个索引,那我们需要用一百个磁盘块来存储,二分查找也就是需要7次的I/O访问,这样需要两百多毫秒,虽然还是很长,但是最起码有进步了。

稠密索引

稀疏索引

多级索引

  为了解决这个问题,我们想一开始对待顺序文件那样对待索引文件,也就是对索引文件建立索引,为了便于识别我们将索引文件的索引称为外层索引,原先的顺序文件的索引称为内层索引。外层索引很小了,我们假设它已经在内存里面了,那么我们先在外层索引中搜索,然后只需要读取一次索引块就可以了,这样就是一次I/O操作。当然了,如果记录实在太多,我们还可以建立三级甚至多级索引,利用多级索引当然是要比二分搜索快得多啦。

  然而,我们要冷静!因为我们所有的讨论建立在一个致命的基础上,也就是没有“溢出块”。而实际上,随着文件的增大,插入删除是很多的,所有“溢出块”还是会不可避免的产生,而一旦存在溢出块就很麻烦了,因为我们只能顺序线性查找了,效率大打折扣。虽然我们可以通过文件重组的方式来消除“溢出块”,但是重组是很占用时间的事情,所以我们不希望频繁重组。在这种情况下呢,被我们隐藏在后台很久很久的B树就横空出现了。设计B树的主要考虑是为了在数据插入或者删除的情况下仍然可以保持效率。当然了,后人将B树稍作改进,也就形成现在使用最为广泛的所有结构---B+树。至于B+树的具体操作,我们就不多提了,大家可以看看其他一些资料。

  3.2.2 从多级索引到B+树

B+树的非叶节点实际上就是一个多级(稀疏)索引,B+树是一种平衡树,也正是这种平衡属性确保了其具备良好的查找删除插入性能。B+树的每个节点大小一般等于磁盘块的大小,而叶节点一般会存储实际记录的磁盘块而不是指向记录的指针。这样一来的话呢,I/O的访问次数就等于树的高度了,而且最关键的一点,我们也不怕溢出块了。

  我们可以大概计算一下B+树的性能。我们假设搜索码32字节,然后磁盘指针8字节,这样一个磁盘块可以装得下10个索引记录。B+树要求非叶节点至少要”半满“,在我们这种情况下也就是每个非叶节点至少有50个索引记录。假设我们有一千万个记录,那么也就是一百万个磁盘块,也就是一百万个叶子节点。树的高度也就是log(50)(1000 000),大概为4,也就是四次I/O操作!四次I/O访问就可以锁定一千万条记录里面的一个记录,你敢信?????哈哈

  我们回过头来看看B+树,为什么它非要确保非叶节点”半满“呢,或者说白了我们为什么不用平衡二叉树?平衡二叉树的操作可是要简单不止一点点啊!我们也按照上面的方法计算一下,一百万个叶子节点的平衡二叉树大概多高--二十。也就是需要二十多次的I/O访问。这下子明白了吧,B+树这个奇怪要求就是为了使得树”矮胖“一点,这样需要的I/O次数才少!

索引还有很多种方式啦,除了上面说的顺序索引之外还有散列索引,位图索引等等形式,我就不一一介绍了。哈哈,说到这里,应该把整个脉络梳理的差不多了,先这样呢,该吃饭去了。

4. 参考

  Database.System.Concepts(6th.Edition.2010)].Abraham.Silberschatz

最新文章

  1. SQL Server通过File Header Page来进行Crash Recovery
  2. BroadCast小结
  3. windows命令行下简单使用javac、java、javap详细演示
  4. GTD3年来读的52本书
  5. linux socket高性能服务器处理框架
  6. noi 8465 马走日
  7. PHP用Array模拟枚举
  8. Web 应用性能提升 10 倍的 10 个建议
  9. response.setHeader("Content-disposition","attachment;filename="+fileName) 下载时文件名中存在空格错误
  10. bootstrap(响应式)加减输入框
  11. 【转】H.264RTP封包原理
  12. Android Q 变更和新特性
  13. Pyqt5+python+ErIC6+QT designer
  14. 48- java Arrays.sort和collections.sort()再次总结
  15. 让div固定在顶部不随滚动条滚动【转】
  16. powerdesidgner1
  17. Linux配置自动时间同步
  18. C++ cout 格式化输出方法
  19. iis6下配置支持.net4.0&发布网站[转]
  20. Go 学习之路:异常处理defer,panic,recover

热门文章

  1. 初级字典树查找在 Emoji、关键字检索上的运用 Part-3
  2. 微信小程序在当前页面设置其他页面的数据
  3. 如何使用淘宝 NPM 镜像,安装CNPM的方法
  4. PAT甲题题解-1022. Digital Library (30)-map映射+vector
  5. Linux内核分析——第二周学习笔记20135308
  6. 个人作业-Week 2 代码复审
  7. thinkphp5报错
  8. Docker HUB 的重要性
  9. Java之数据流-复制二进制文件
  10. BZOJ2653 middle(二分答案+主席树)