参考文章:
MySQL索引背后的数据结构及算法原理
摘要
概念
索引是“帮助MySQL高效获取数据”的“数据结构
数据库查询:顺序查找、二分查找、二叉树查找,每种查找都只能应用在特定的数据结构上。
因此,数据库系统,除了保存数据之外,还维护着满足特定查找算法的数据结构;
这些数据结构的每个节点并不存储数据记录本身,而是以某种方式引用(指向数据)。
这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
看一个例子
左边是真实的存储数据的数据表,数据存储时不一定相邻;
右边维护了一个二叉查找树的索引数据结构,每个节点包含一个索引键值,和一个指向对应“数据记录”物理地址的指针;这样就可以运用二叉查找树来加快数据查找。
这是一个货真价实的索引,但是实际的数据库几乎没有使用二叉查找树或红黑树实现的。
二叉查找树,(红黑树),平衡二叉查找树,B树,B+树,B*树
目前大部分数据库系统及文件系统都采用B树或者B+树作为索引结构
1.B树
概念:又称多路平衡查找树,B树中所有节点的孩子节点数的最大值称为B树的阶,通常用m表示。
一棵m阶B树可以为空树,或满足如下特性的m叉树:
树中每个节点最多有m棵子树,即最多含有m-1个关键字
若根节点不是终端节点,则至少含有两棵子树
除根节点外的所有非叶节点至少含有[m/2] 棵子树,至少含有[m/2]-1个关键字(向上取整)
非叶节点的结构如下:每个节点包含k个关键字,k+1个指针;关键字Ki左边指针指向的子树中所有节点的关键字均小于Ki,右边指针指向的子树中所有节点的关键字均大于Ki
所有的叶子结点都出现在同一层,并且不带信息(可以视为外部节点活在这类似于折半查找判定树的查找失败节点,实际上这些节点不存在,只想这些节点的指针为空)
B树是所有节点的平衡因子均等于0的多路查找树。
B树的检索:在B树中查找节点,再在节点内二分查找关键字
算法:首先在根节点内二分查找,如果找到则返回对应的data;否则对相应区间的指针指向的节点递归查找;直到找到节点或只找到null指针,前者查找成功,后者查找失败。
伪代码:
BTree_Search(node.key){
//递归出口
if(node==null) return null;
//查找本节点
for(int i=1;i<=m;i++){
if(node.key[i]==key) return node.data[i];//返回数据记录(实际为物理指针)
else if(node.key[i]>key) return BTree_Search(point[i]->node);//左子树查找
else return BTree_Search(point[i+1]->node);//右子树查找
}
}
data=BTree_Search(root,my_key);
B树的插入、删除此处不讨论;
B+树
B+树是B树的变种,MySQL普遍采用B+树实现其索引结构
一棵m阶的B+树应该满足下列条件:
每个分支节点最多有m棵子树(子节点)
非叶根节点至少有两棵子树,其他每个分支节点至少有[m/2]棵子树(向上取整)
节点的子树个数和关键字个数相等
所有叶节点包含全部关键字及指向相应记录的指针,叶节点中将关键字按大小排序排列,并且相邻叶节点按大小顺序相互链接起来。
所有分支节点(可视为索引的索引)中仅包含它的各个子节点(即下一级的索引块)中关键字的最大值及指向其子节点的指针。
m阶B+树和m阶B树的主要差异如下:
在B+树中,具有n个关键字的节点含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的节点含有n+1棵子树
在B+树中,每个节点(非根内部节点)的关键字个数n的范围是[m/2]<=n<=m(根节点:1<=n<=m)。在B树内,每个节点(非根内部节点)的关键字个数n的范围是[m/2]-1<=n<=m-1(根节点:1<=n<=m-1)
在B+树中,叶节点包含信息,所有非叶节点仅起到索引所用,非叶节点的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
在B+树中,叶节点包含了全部关键字,即在非叶节点中出现的关键字也会出现在叶节点中;而在B+树中,叶节点的关键字和其他重复节点的关键字是不重复的。
B+树的查找
在B+树中有两个头指针,一个指向根节点,一个指向关键字最小的叶子结点。因此可以对B+树进行两种查找,一种是从根节点开始的多路查找,一种是从最小关键字开始的顺序查找。
多路查找过程中,当非叶节点上的关键字等于给定值时并不终止,而是继续向下查找,直到找到叶节点上的该关键字为止。
因此在B+树中查找,无论查找成功与否,每次查找都是从根节点到叶节点的路径
B+树的每个叶子结点有一个指向相邻叶子节点的指针,提高了区间访问的性能。
红黑树等数据结构也可以用来实现索引,结合计算机组成原理讨论选择B树或B+树的原因。
索引本身一般也很大,索引往往以文件的形式存储在磁盘上;
这样索引查找过程中就要产生磁盘IO,相对于内存存取,磁盘IO的时间消耗要高几个数量级;
所以评价一个数据结构作为索引的优劣:索引的数据结构要尽量减少磁盘IO的次数
内存的存取原理
主存目前主要是RAM
磁盘的读取原理
磁盘读取存在机械运动费时,磁头先定位到相应盘片,再定位到磁道(寻道时间),最后定位到扇区(旋转时间);每个扇区是磁盘的最小存储单元。
当需要从磁盘读取数据时,系统会将物理地址传给磁盘,磁盘中的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定盘片,磁道,扇区。
局部性原理与磁盘预读:
预读:为了尽量减少磁盘IO,磁盘往往不是严格按需读取,而是每次都会预读;即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。
局部性原理:程序运行期间所需要的数据同城比较集中。当一个数据用到时,其附近的数据也通常会马上被使用。
由于磁盘的顺序读取效率很高(不再需要寻道时间,只需要很少的旋转时间),因此对于局部性较好的的程序来说,预读可以提高IO效率
页面:预读的长度一般为页(page)的整数倍。
操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块成为一页(通常为4K),主存和磁盘以页为单位交换数据。
当程序要读取的数据不在主存中是,缺页中断。
B树的索引性能分析
索引数据结构的优劣取决于磁盘IO次数;
在B树中,每次检索都访问不超过h个节点(h为B树高度);并且数据库系统巧妙地利用磁盘预读原理,将每个节点的大小设为一个页,这样每个节点只需要一次磁盘IO就可以完全载入。
为了达到这个目的,在实际实现B树时还需要使用如下技巧:
综上所述,用B树作为索引结构效率是非常高的。
而用红黑树,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的IO复杂度也比B树差很多
相对于B树,B+树更适合索引,原因和内节点出度m相关。
从上面的分析来看,m越大,h越小,性能越好;B树和B+树差不多。
B+树的内节点只存储子树的最大值关键字,只有叶节点存储记录数据,因此每次都会进行h次磁盘IO,但是B+树所有叶子结点都链接起来了,便于读取局部性连续数据。
在MySQL中,不同的存储引擎索引的实现方式不同。
MyISAM的索引文件仅仅保存数据记录的主键和物理地址;
在MyISAM中,主索引和辅助索引在结构上没有区别;只是主索引要求key是唯一的,而辅助索引key可以重复。
如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:
MyISAM中索引检索:
首先按照B+树搜索算法搜索索引,如果指定的key存在,则取出data域的值(相应记录的物理地址);
然后再去数据存储区域,读取相应的数据记录。
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的“聚集索引”区分
虽然InnoDB也使用B+树作为索引结构,但是具体的实现方式和MyISAM截然不同。
区别一:InnoDB的数据文件本身就是索引文件。
MyISAM中索引文件和数据文件是分离的,索引文件仅仅保存了数据记录的地址。
而InnoDB中,数据文件本身就是按照B+树组织的一个索引结构;这棵树的叶节点data域保存了完整的数据记录。
索引的key就是数据表的主键,因此InnoDB表数据文件本身就是主索引文件。
可以看到叶节点包含了完整的数据记录,这种索引叫做“聚集索引”
因为InnoDB的数据文件本身就是按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有);如果没有显式指定,MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键。
区别二:InnoDB的辅助索引data域存储相应数据记录主键的值,而不是物理地址。
换句话说,InnoDB所有辅助索引都引用主键作为data域。
例如,下图为定义在Col3上的一个辅助索引
检索:
聚集索引,使得主键的检索十分有效;
而辅助索引检索,需要检索两边索引,首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的“索引实现方式”对于“正确使用’和”优化索引“都非常有帮助;
MySQL的优化主要分为结构优化和查询优化。此处讨论的“高性能索引策略”属于结构优化范畴。
示例数据库:
联合索引的概念:MySQL的索引可以以一定顺序引用多个列,一般一个联合索引是一个有序元组
另外单列索引可以看成联合索引元素数为1的特例。
全列匹配
当按照索引中所有列进行精确匹配(这里的精确匹配指“=”或“IN”匹配)时,索引可以被用到。
理论上索引对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引。
最左前缀匹配
使用了索引,但是key_len为4,说明只使用到了索引的第一列前缀
查询条件用到了索引中列的精确匹配,但是中间某个条件未提供
后面的from_data虽然也在索引中,但是由于title不存在而无法与左前缀链接。
可以手工补充中间缺失列,如果中间列元素较少且已知:
查询条件没有指定索引第一列
匹配某列的前缀字符串
范围查询
范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。
同时索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。
explain无法区分范围索引和多值匹配,用“between”并不意味之范围查询,相当于多值匹配“IN”
查询条件中含有函数或表达式
此时MySQL不会为这列使用索引
问题:索引可以查询速度,但是也有代价:索引文件本身要消耗存储空间;同时索引会加重插入、删除、和修改记录时的负担、另外,MySQL在运行时也要消耗资源维护索引;
因此,索引并不是越多越好,一般两种情况下不建议使用索引。
表记录较少时,没必要建立索引;
让查询做全表扫描就好,阈值在2000左右
当索引的选择性较低时,不建议建立索引;
索引的选择性:指不重复的索引值与表记录数的比值。
当重复键值太多,索引价值不大。
为列建立索引时的一个优化策略:前缀索引——用列的前缀替代整个列作为索引的key。
当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引;同时因为索引key变短而减少了索引文件的大小和维护开销。
主键选择
在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。
与插入优化
InnoDB使用聚集索引,数据记录本身被存在主索引(一个B+树)的叶子结点上。
这就要求同一个叶子结点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放;
因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点及其位置中;如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)
如果表使用自增主键,那么每次插入新的记录,记录就会在当前索引叶子节点的最后面添加,当一页写满,就会自动开辟一个新的页。
这样就会形成一个紧凑的索引结构,近似顺序填满。
由于每次插入也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
如果表使用非自增主键(如身份证号或学号),由于每次插入主键的值近似于随机,因此每次新纪录都要被插入到现有索引页的中间某个位置:
此时MySQL不得不为了将新记录插入到合适位置而移动数据。
甚至目标页面可能已经被写到磁盘上而从缓存中清理掉,此时又要从磁盘上读回来,这增加了很多开销;同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
因此,只要可以,请尽量在InnoDB上采用自增字段做主键。