咱们常说的“数据库”,好比“MySQL”、“Oracle”等,其实严格来讲是DBMS(Database Management System),数据库只是一个存储数据着数据的仓库,而DBMS作的事是让咱们可以操做数据库,好比解析SQL、DML等,都是DBMS在支持着。 在DBMS之下,又有着存储引擎,为DBMS提供数据增删改查的支持,不一样的存储引擎提供不一样的特性,负责组织数据的就是存储引擎。算法
每个字段都须要定义一个数据类型(DataType),数据类型在数据组织时的意义是肯定数据长度,存储介质将会为其分配合适长度的空间。 每一个字段被按照顺序组织起来,而且在开头存储着这一行的某些头信息(MetaData),例如记录总长度、时间戳等,这就组成了一条在硬盘上被存储的完整记录: 数据库
行是一条具备完整意义的记录,被按照必定的规则,依次存储在文件中。 记录在文件中有如下几种主要的组织方法:缓存
记录与记录之间没有顺序关系,每条记录能够存放在文件中的任何地方,只要想被存储的地址有足够空间。数据结构
也就是遵循某个搜索码(Search key)的顺序,依次存储每一条记录。搜索码是一系列搜索条件的组合,能够是一个键,也能够是多个键组合。以下图,按照第二列,也就是姓名的顺序去决定记录的存储顺序: 数据结构和算法
前面提到的顺序文件是不一样的表存储在不一样的文件中,可是某些具体应用场景下,可能经常涉及多表查询,好比有一个名为Singers的表保存着歌手信息,又有一个名为Albums的表保存着每一个歌手发布的专辑信息,若是你正在开发一个音乐播放器,那么涉及的场景通常都是须要找出某个歌手发布的全部专辑展现给客户,若是不一样的表保存在不一样的文件中,那么须要进行链接(Join) ,复杂度比较高,可是若是将每一个歌手的专辑信息都在物理上存储在歌手信息以后,也就是两张表混合存放在同一个文件中: 函数
散列文件彻底没有顺序,每条记录应该存放的位置,是根据搜索码的Hash值决定的,所以插入删除都不涉及记录移动,且因为搜索码的Hash值直接决定了存储位置,因此查找符合特定搜索码的记录很是快,可是不支持范围查找与顺序读取。大数据
DBMS维护着本身的缓存空间,使用一些缓存置换算法尽可能确保那些常常被使用的数据在缓存中,以免磁盘的读取。与DBMS同样,磁盘通常也有着本身的缓冲区以保存常常被读取的数据,减小响应时间。所以,若是要读取一条记录,根据优先顺序,路径为DBMS缓存区 => 磁盘缓存区 => 磁盘。优化
这是成本最低的方式,由于DBMS缓存区就在内存,能够直接被CPU使用,不涉及磁盘IO,能够考虑IO时间为0。操作系统
若是磁盘缓存区有须要的记录,则只须要直接读出,传输时间考虑为1ms。设计
因为SSD比较贵,经常使用的仍是机械硬盘,对于机械硬盘,要读取指定地址的数据,是须要通过寻道的,机械臂须要先移动到指定位置,所以不管读取多少数据,准备工做都会耗费一段时间。 整个IO流程包括:排队等待 => 寻道 => 半圈旋转 => 传输
考虑一种状况:咱们有一张存储着100万个注册用户的Users表,咱们要搜索用户名为AfterShip
的用户,若是这张表是使用顺序文件存储,而且存储顺序是根据account_id
列,而不是根据username
列,在没有索引时,查找的方式应该是从第一条记录起依次读入记录,并对比每一条记录的username
是否为AfterShip
,直到找到为止。最好的状况是第一条记录即符合要求,最坏的状况是最后一条记录才符合要求,在最坏的状况下,须要读取100万条记录,假设每条记录1kb,须要读取976MB的数据!即便以200MB/s的传输速度,仅仅是IO时间就须要5s读取记录,而且还须要大量的时间给CPU处理100万条的记录。 若是是以account_id
做为搜索条件,最快的方式是从文件的最中间位置读出最中间记录,对比account_id
的大小,再判断往前仍是日后读,也就是使用2分搜索,最坏的状况下须要进行logN次,也就是20次左右的随机读,耗时200ms。 所以,当咱们的搜索码被顺序地组织起来,咱们就能更少地读取数据,以更快的方式查询到符合要求的记录,可是,文件只能以一种搜索码组织起来,不能既以account_id
为顺序,又以username
为顺序,所以,咱们须要一种冗余的数据——索引,来以咱们想要的顺序组织某个搜索码,加速咱们的查询。
索引是一种被以合适的数据结构组织起来方便搜索的冗余数据,也存储在文件中。 好比对于Users表,咱们为username
创建索引,那么DBMS会将username
的值复制一份,并排序,保存在一个文件中:
username
为
AfterShip
的记录,就可使用二分搜索,或者哪怕是顺序扫描整个索引也比以前进行全表扫描快得多,由于一个username的长度若是是50bytes,那么扫描整个索引也只须要读取不到50MB的索引文件,体积只是全表扫描的二十分之一。 因而可知,索引能够有效地加快查询速度。刚刚讲到的是顺序索引,在索引的具体实现中还有多种更复杂的数据结构和算法,索引有多种实现方式,每种实现方式都各有优缺点,适应不一样的应用环境。
索引能够从多个维度分类,每一个维度的分类互不冲突。
username
创建的索引。查找速度: 对于稠密索引,因为为每个搜索码都创建的相应的索引项,所以空间占用比较大,可是查找速度较快,由于能够从索引文件中直接找到对应记录的位置,而使用稀疏索引须要先找到记录所在的页,再读出整个页,从页中找到具体的记录。 维护成本: 稠密索引为每一个搜索码都创建对应的索引项,且索引项中还保存着符合此搜索码的全部记录的指针,也就是关联到了表中的每一条记录,所以当任何一条记录被删除、插入,都须要修改甚至移动、重组索引文件,维护成本较高。 而稀疏索引仅仅为分组创建索引项,当组中有记录删除时不必定会立刻修改索引,有记录插入到现有的组时,只要不占用新的页或者影响到组的第一条记录,那么也不会创建新的索引,索引更新相对不那么频繁,维护成本较小。
继续考虑那张存有100万条用户数据的Users表,咱们为username
创建了有序稠密索引,而且咱们假设username
是具有惟一性的,也就是对于100万个用户,就有100万个不一样的username
,稠密索引将会有100万条索引项,若是一个4kb的页能保存100条索引项,那么就须要1万页来保存整个索引文件。若是咱们要查询username
为AfterShip
的用户,使用二分法就须要进行logN次的查询,也就是14次随机读找到其索引项,再经过一次随机读读出记录,一共150ms,一秒内只能进行6次查询。若是咱们能减小其随机读次数,那么每少一次随机读,就会少10ms的耗时,减小随机读有如下两个思路:
若是咱们基于100万条稠密索引再去创建稀疏索引,也就是对1万个页创建索引,那么对于一页能保存100条索引项的状况下,咱们将会有更上一级的,仅占用100页的稀疏索引,整个索引文件为400kb,足够小到可以放入内存,所以能够保存在DBMS缓存区,先经过稀疏索引找到稠密索引所在的页地址,再进行一次随机读,读出整个页,找到搜索码对应的具体索引项,而后再进行第二次随机读,读出表中记录,一共只有1次内存读+2次随机读,20ms。仅仅多创建一层稀疏索引,也便是使用二级索引结构,就有7倍的效率提高。在现实场景中,每每会屡次进行这种索引结构的创建,也就是多级索引结构。
B+树是一种多级索引的实现,采用平衡树结构,有非页节点、叶节点两种节点组成,每种节点存放的数据有细微差异:
非叶节点(根节点也是非叶节点): 节点最多包含着n-1个搜索码值K1…Kn-1,并包含着n个指针P1…Pn,也就是两边是指针,中间是搜索码值,Pn指针指向小于其Kn搜索码的下一级索引节点,Pn+1指向大于等于Kn搜索码的下一级索引节点。
叶节点 叶节点的P1…Pn-1的指针都指向记录地址(若是是稠密索引)或者页地址(若是是稀疏索引),叶节点的最后一个指针Pn与非叶节点不一样,它指向的是下一个同级叶节点,构成横向有序的索引结构。
一个完整的三级B+树以下所示:
B+树维护成本 考虑一个稠密B+树索引,在删除记录时,因为B+树要求每一个叶节点都必须处于半满状态,当被删除索引项所处的节点不知足半满时,须要向兄弟节点借搜索码值,而且在须要时调整父节点,是一个局部重组B+树的过程。 在插入记录时,可能出现某个索引节点已经没有多余空间存储,此时则须要分裂叶节点,而且上层非叶节点也可能须要分裂,依次往上递归,也是一次重组的过程。
B+树的优势 从上面可以看出,在某些状况下,删除和插入记录时,B+树的维护成本比较高,可是为什么依旧是最经常使用的索引结构之一呢,由于咱们每每会把每一个节点的空间设置得足够大,通常是一整页,若是一个索引项占用100bytes,则对于4kb的页可以存储40个索引项,即便是100万条记录的表,B+树也只须要log(40)1000000=3层,查询路径很是短,所以B+树其实是一种效率很是高的索引结构。
####(2)B树索引 这是一种与B+树相似的平衡树索引,区别在于,B+树只有叶节点保存着指向记录的指针,非叶节点仅仅是索引着索引的索引,而B树整棵树的全部节点都保存着指向其对应记录的指针,整棵树才是一个完整的索引:
前面讲到的索引结构都须要经过对比搜索码的大小去查找索引项的位置,复杂度是对数级别的,而散列索引将存储空间分为多个组,称为桶(Bucket),直接经过散列函数计算搜索码的Hash值,经过Hash值肯定此搜索码的索引项在哪一个桶中,读取桶中的索引项,就能够找到对应索引项,复杂度为O(1),所以散列索引对于查询指定搜索码的效率很是高。 根据桶的数量是否固定,散列索引分为静态散列与动态散列两种:
目前为止咱们讨论的都是搜索码为一个字段的状况,其实搜索码能够是多个字段的组合,好比index(username,age,city),索引项中按照索引定义次序依次存储着三个字段的值,好比(AfterShip,25,ShenZhen),索引项之间的排序先根据第一列索引排序,第一列相同的状况下再根据第二列排序,以此类推。
覆盖索引不是一种索引分类,而是一种对索引的使用方式。 继续考虑上面那张保存着100万用户的Users表,咱们要查找username
为AfterShip
的用户的email
,若是咱们仅仅为username
创建索引,那么咱们须要先经过索引查找到username
为AfterShip
的帐号的记录指针,再回表读取此记录email
列的值。但若是咱们的索引是为(username,email)创建的复合索引,那么咱们在索引项中就能直接获取到email
值,而不须要回表读取,减小一次随机IO操做。 所以,适当地利用覆盖索引,能够减小IO,加快查询。