大学时就开始学习数据库,到工作2年多了,接触过很多数据库相关的产品,如Oracle/MySQL这样的RDBMS,Redis/Mongodb这样的NoSQL,还有Hive/HBase这样的大数据技术。虽然说招式学了很多,而好的程序员应当知道数据库基本原理,索引基本原理,常见优化手段等。花了近一个月的时间对数据库的这些知识进行学习,收获很多,以前数据库在我眼中就是个增删改查的东西,而现在想法发生了很多改变!


组成数据库的2个要素


wKiom1ajFA-BXSznAAALRpdIvyo206.png


数据库中的数据存储在哪里?

可以是磁盘,或者磁盘组成的阵列,或者内存,或者某些存储设备,总之数据应该有地方可以存储。


然而仅仅存储数据是不够的,一块储存有数据的硬盘,能够称之为数据库吗?

应该有这样的程序:封装对存储的操作细节,提供给外界简单的API。这样的程序就是实例。


因此,数据库的启动,应当是实例对存储的服务启动,提供给外界简单的API来完成数据操作。


那么进一步思考下,存储和实例是什么关系呢?是一对一,又或者是多对多呢?


如果,存储有多份,甚至这多份之间还不在一个机房,一个地方,这不就是冗余备份,异地容灾吗?

如果,一个存储对应多个实例,那么将会提高外界对这个存储的负载能力以及达到高可用。一个实例挂了,不用担心,还有其他的实例来确保这个存储对外界的可用性。



数据库的物理与逻辑转换


块的概念


数据库的数据是如何存储的,这非常重要,因为如何存储决定了如何访问数据。

试想,如果按行存储,按行访问,或者我们采用JAVA类似的随机访问的方式,先定位位置后在读取一段内容,数据库会这样做吗?

行,是抽象的,可以很大,也可以很小,粒度是不可控的,而且行还是逻辑上的概念,并不利于数据库进行物理上的调度和管理。那么数据库的数据到底如何存储呢?


wKiom1ajIN7iHctMAAAKDNBv8QQ046.png

数据库会对存储进行物理大小的划分(比如16K),叫做Block(或者Page)。


一个Block内会存储很多行,数据库在加载一个Block时,通常并不会仅仅只加载这个Block,而是会将这个Block周围的Block都加载至内存,因为会显著的提升内存命中率以及减少IO次数。为了方便管理,应该要对Block进行编号,以及Block内的行进行编号。(也许编号并不是唯一的,但是与某些高层次的编号组合起来就是唯一可以定位了,例如Oracle的rowid就是表空间、对象编号,块编号,块内行编号共同组成,因此根据rowid就可以快速定位到数据。)


要知道在文件系统上,并没有Block的概念,Block不过是数据库自认为的东西,那么数据库如何定位以及读取一个Block呢?


wKioL1ajKIKRhN0_AAAE1K14aVk134.png


如果我们有一个LIST,它记录了块的ID,以及块在文件系统中的偏移量,那么我们定位一个块就很简单了。只需要先找到它在文件系统中的偏移量,然后读取固定大小的数据,就相当于读取了该块的内容了。这个LIST,如同很多数据库的元数据信息一样,即数据字典。至于,读取块的内容,当然就是磁盘IO了,这也是数据库提升性能的一大障碍。要知道一块磁盘只有一个磁头,所有的请求都串行化,本身读取的速度并不慢,慢就慢在需要磁头定位以及寻道。因此,我们希望加大内存,加大内存命中率,减少磁盘IO,但是毕竟内存是有限的,必然存在一些数据被加载内存,一些数据从内存中OUT,重新写回磁盘。数据库的这种对内存的管理策略,可能类似LRU最近最少使用,或者其他更加“智能”点的策略。一般数据量较小,并且并不频繁更新的信息,比如数据字典这类数据就可以常驻内存中。


块的层次


wKiom1ajLaawmSa6AABC6JCTsWs620.png

我们知道每个学生都有一个学号,但是仅仅有一个学号是不够的,通常学生都是XXX年级YYY班级的,对学生进行一个层次的抽象,这样将方便学生的管理!而数据库中块也是一样的,如果将多个块组成一个group,多个group组成一个segment,多个segment组成......当块形成了层次后,自然方便了更高层次的数据管理,比如一个表的多个分区的管理。


表的修改以及删除


如果一个块的大小是16K,一行数据100Byte,也就是块内至多可以储存160行数据。

假设我们要对表结构进行修改,增加一个字段,并且修改后,立即填充此字段的值,那么会发生什么?


如果块内数据存储是满的,那么自然要在新块中进行存储,也就是说一行数据被分在了不同块中进行存储,那么本来读取一个块就可以知道一行数据的,现在变成了2个块,也即是增加了IO的次数。也就是在这种情况下,出现了这样的情况:修改表的结构竟然使得读取变慢了!


如果块内数据存储不是满的,即本身块内是预留了一部分空间,那么可能增加一个较小的字段,还能存放下,增加较大的字段,那么还是会出现上面假设的情况。


假设我们要删除一个表的数据呢?

表的数据存在哪里呢?块里面有,内存里面也有吧!

若逐行删除数据,会很慢,为何?


既然要按照行去删除数据,那么必须要先找到数据,那么自然涉及到块的定位,行的定位等;而如果基于块的体系结构去删除,那么肯定会快很多;如果数据对应的就是文件的话,那么直接删除文件好了。

当然有些数据库的删除表数据,是一种“假删除”,会更加快,而且删错了还可以“吃后悔药”。这种“假删除”,只需要将原表的元数据的状态置为不可用,让外界访问不到它,如果想恢复,那么很简单,只需要重置它的状态为可用就可以了。


那么内存中的数据如何处理呢?

为了使得每次访问数据,不至于都发生磁盘IO,因此内存中会有一部分数据存在,如果将数据库的内存配置很大,比如48G,会装入很多数据,这种情况下如果进行内存区域的全部扫描,也会很花费时间,那么如何快速定位一个表在内存中的数据呢?


wKioL1ajQwvAvzIGAAA4FmTTAhg488.png



预编译SQL


作为Java开发,我们经常说对数据库的SQL应该要采取PreparedStatement这种预编译的方式,不要采用字符串拼接的方式,因为可以防止SQL注入以及更加快。那么为什么预编译SQL会快呢?


数据库拿到SQL后,应该要对其进行解析操作,要将SQL转变成操作,是读,是写,什么表,表与表什么关系,什么字段,什么条件等等。


如果每来一个SQL,都进行解析操作,那么在高并发的系统当中,是否可以优化呢?对于像Oracle这样的数据库,对于SQL有“软解析”和“硬解析”。如果将有些SQL的解析结果缓存起来,当下次还有这样的SQL请求时,就可以不用做解析操作,直接拿取缓存中的解析结果就可以了,这样的SQL解析即是“软解析”。而对于采用字符串拼接的SQL,数据库会认为每次SQL都不同,因此都会进行解析操作,这就是“硬解析”。要知道SQL解析结果的缓存也是存放在内存中,因此也有大小限制,对于硬SQL,会频繁对SQL解析缓存IN/OUT,导致本该留在SQL解析缓存的SQL被替换,而软解析很好的避免了这种情况。


为何要小表驱动大表?


以前经常遇到这样的情况:有2张表T1,T2,T1数据量较小(比如几十条),T2数据量很大(比如千万级别)。我们说应该让T1作为驱动表,这样会快很多。


为什么会快很多呢?谁作为驱动表有关系吗?M*N和N*M大小不是一样的吗?这根本上说不通啊。


实际上是这样的,我们希望小表驱动大表,是想外层循环用小表,这样外层循环的次数很少,内层循环用大表,希望大表的查询去走索引,即使大表很大,走索引的话,也会很快。因此这才是小表驱动大表快的根本原因。反之,如果大表作为外层循环,那么即使小表走索引,由于外层循环次数太大,也会很慢的。



索引的基本原理


索引就像是一本书的目录一样,能让我们快速的找到具体的章节,但是如果增加了书的内容后,也得更新目录。目录通常应该是有序的,目录让查找变快,也让增加变慢。


那么为什么索引有这样的作用呢?


索引,是一种结构,比如主流的B+树索引,非主流的HASH索引;索引是一种数据,也需要储存起来。下面我们就来简单分析下B+树索引的基本原理。


索引,也是储存在Block上,比如对于A表的F字段上建立了索引的话,那么索引通常会存储该字段的值,以及对应的数据位置,以便和数据内容联系起来。要知道,索引存储的宽度要比数据存储区域的宽度小的多,因此一个索引Block往往会存储上万级别的数据,比数据Block要多很多。

wKiom1akQ7aQtAR5AAAWyv6Vxjo789.png

如果select F from A where F = 1,那么会怎么样呢?


要知道F上建立了索引,如果我们直接走索引查找的话,那么遍历索引存储区域,只需要读数量很小的Block就可以得到了,而且在索引上就有F值,因此查找到就可以直接返回了。而如果走数据存储区域查找的话,那么要遍历数量巨大的Block。


我们再来看一条SQL:select F , P from A where F = 1,又会怎么样呢?(P只是普通字段)

当然我们可以先走索引确定数据范围,但是由于索引上并没有P值,此时就得“回表”查找,也就是利用索引查找到的位置,到源表中取得数据。


此时此刻,我们应该对索引有所体会了,但是好像没有什么树的概念?


随着数据量的增大,当然索引也会变大,会变成多层次的结构,如同数据块一样!


wKioL1akSKjjFH9MAAA97M_DuMY207.png


到这里,好像有点树的形象了,实际上出现了索引管理块的概念,也许随着索引数据的增多,会出现多层次的索引管理块。真正存放索引数据的块,称为叶子块,叶子块存有双向指针,主要是为了快速遍历而设计的。比如:

select count(F) from A

F在A表上是建立了索引的,那么找出F的数量,很简单,就是遍历叶子块大小而已,而通过双向指针又加速了遍历,所以很快咯。


结束语

以前,我们所相信的,认为的那些事情,是有原因的,有场景的,我想只有懂一点数据库基本原理,才能更好的运用到工作上。