MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)

MySQL索引详解

参考文章:
MySQL索引背后的数据结构及算法原理

摘要

  • MySQL支持多种数据引擎,各种数据引擎对索引的支持各不相同;因此MySQL数据库支持多种索引类型,如B+树索引、哈希索引、全文索引等。本文只讨论B+树索引
  • 文章分为三个部分:
    1. 索引的本质(B树和B+树)
    2. 结合MySQL数据库中的MyISAM和InnoDB存储引擎的索引实现,讨论聚簇索引、非聚簇索引
    3. 讨论MySQL中索引的使用策略、是否为字段建立索引及建立索引的优化策略、主键选择及记录插入优化

一、索引的本质、B树与B+树

(一)索引的本质

  1. 概念

    • 索引是“帮助MySQL高效获取数据”的“数据结构

    • 数据库查询:顺序查找、二分查找、二叉树查找,每种查找都只能应用在特定的数据结构上。

      因此,数据库系统,除了保存数据之外,还维护着满足特定查找算法的数据结构

      这些数据结构的每个节点并不存储数据记录本身,而是以某种方式引用(指向数据)。

      这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

  2. 看一个例子

MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第1张图片

左边是真实的存储数据的数据表,数据存储时不一定相邻;

右边维护了一个二叉查找树的索引数据结构,每个节点包含一个索引键值,和一个指向对应“数据记录”物理地址的指针;这样就可以运用二叉查找树来加快数据查找。

这是一个货真价实的索引,但是实际的数据库几乎没有使用二叉查找树或红黑树实现的。

(二)B树和B+树

二叉查找树,(红黑树),平衡二叉查找树,B树,B+树,B*树

目前大部分数据库系统及文件系统都采用B树或者B+树作为索引结构

1.B树

  • 概念:又称多路平衡查找树,B树中所有节点的孩子节点数的最大值称为B树的阶,通常用m表示。

  • 一棵m阶B树可以为空树,或满足如下特性的m叉树:

    1. 树中每个节点最多有m棵子树,即最多含有m-1个关键字

    2. 若根节点不是终端节点,则至少含有两棵子树

    3. 除根节点外的所有非叶节点至少含有[m/2] 棵子树,至少含有[m/2]-1个关键字(向上取整)

    4. 非叶节点的结构如下:每个节点包含k个关键字,k+1个指针;关键字Ki左边指针指向的子树中所有节点的关键字均小于Ki,右边指针指向的子树中所有节点的关键字均大于Ki

    5. 所有的叶子结点都出现在同一层,并且不带信息(可以视为外部节点活在这类似于折半查找判定树的查找失败节点,实际上这些节点不存在,只想这些节点的指针为空)

      B树是所有节点的平衡因子均等于0的多路查找树

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第2张图片

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第3张图片

  • 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树的插入、删除此处不讨论;

  1. B+树

    • B+树是B树的变种,MySQL普遍采用B+树实现其索引结构

    • 一棵m阶的B+树应该满足下列条件:

      1. 每个分支节点最多有m棵子树(子节点)

      2. 非叶根节点至少有两棵子树,其他每个分支节点至少有[m/2]棵子树(向上取整)

      3. 节点的子树个数和关键字个数相等

      4. 所有叶节点包含全部关键字及指向相应记录的指针,叶节点中将关键字按大小排序排列,并且相邻叶节点按大小顺序相互链接起来。

      5. 所有分支节点(可视为索引的索引)中仅包含它的各个子节点(即下一级的索引块)中关键字的最大值及指向其子节点的指针

    • m阶B+树和m阶B树的主要差异如下:

      1. 在B+树中,具有n个关键字的节点含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的节点含有n+1棵子树

      2. 在B+树中,每个节点(非根内部节点)的关键字个数n的范围是[m/2]<=n<=m(根节点:1<=n<=m)。在B树内,每个节点(非根内部节点)的关键字个数n的范围是[m/2]-1<=n<=m-1(根节点:1<=n<=m-1)

      3. 在B+树中,叶节点包含信息,所有非叶节点仅起到索引所用,非叶节点的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。

      4. 在B+树中,叶节点包含了全部关键字,即在非叶节点中出现的关键字也会出现在叶节点中;而在B+树中,叶节点的关键字和其他重复节点的关键字是不重复的。

        MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第4张图片

    • B+树的查找

      • 在B+树中有两个头指针,一个指向根节点,一个指向关键字最小的叶子结点。因此可以对B+树进行两种查找,一种是从根节点开始的多路查找,一种是从最小关键字开始的顺序查找

      • 多路查找过程中,当非叶节点上的关键字等于给定值时并不终止,而是继续向下查找,直到找到叶节点上的该关键字为止。

        因此在B+树中查找,无论查找成功与否,每次查找都是从根节点到叶节点的路径

    • B+树的每个叶子结点有一个指向相邻叶子节点的指针,提高了区间访问的性能。

(三)索引的数据结构为什么选择B树或者B+树

红黑树等数据结构也可以用来实现索引,结合计算机组成原理讨论选择B树或B+树的原因。

  • 索引本身一般也很大,索引往往以文件的形式存储在磁盘上;

    这样索引查找过程中就要产生磁盘IO,相对于内存存取,磁盘IO的时间消耗要高几个数量级;

    所以评价一个数据结构作为索引的优劣:索引的数据结构要尽量减少磁盘IO的次数

  1. 内存的存取原理

    主存目前主要是RAM

    • 主存是由一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据,每个存储单元有唯一的地址;

      MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第5张图片

      此处简化了一个二维地址,通过行地址和列地址可以唯一定位一个存储单元,图示为一个4*4的主存模型。

    • 主存的存取过程:

      地址信号定位存储单元,将存储单元的数据放到数据总线上,供外部读写。

      存取过程较快。

  2. 磁盘的读取原理

    • 磁盘读取存在机械运动费时,磁头先定位到相应盘片,再定位到磁道(寻道时间),最后定位到扇区(旋转时间);每个扇区是磁盘的最小存储单元。

    • 当需要从磁盘读取数据时,系统会将物理地址传给磁盘,磁盘中的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定盘片,磁道,扇区。

    • 局部性原理与磁盘预读:

      预读:为了尽量减少磁盘IO,磁盘往往不是严格按需读取,而是每次都会预读;即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。

      局部性原理:程序运行期间所需要的数据同城比较集中。当一个数据用到时,其附近的数据也通常会马上被使用。

      由于磁盘的顺序读取效率很高(不再需要寻道时间,只需要很少的旋转时间),因此对于局部性较好的的程序来说,预读可以提高IO效率

    • 页面:预读的长度一般为页(page)的整数倍。

      操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块成为一页(通常为4K),主存和磁盘以页为单位交换数据。

      当程序要读取的数据不在主存中是,缺页中断。

  3. B树的索引性能分析

    • 索引数据结构的优劣取决于磁盘IO次数;

    • 在B树中,每次检索都访问不超过h个节点(h为B树高度);并且数据库系统巧妙地利用磁盘预读原理,将每个节点的大小设为一个页,这样每个节点只需要一次磁盘IO就可以完全载入。

    • 为了达到这个目的,在实际实现B树时还需要使用如下技巧:

      1. 每次新建节点时,直接申请一个页的空间
      2. B树中一次检索最多只需要h-1次磁盘IO(根节点常驻内存);一般而言,h=logmN,h会比较小

      综上所述,用B树作为索引结构效率是非常高的。

      而用红黑树,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的IO复杂度也比B树差很多

    • 相对于B树,B+树更适合索引,原因和内节点出度m相关。

      从上面的分析来看,m越大,h越小,性能越好;B树和B+树差不多。

      B+树的内节点只存储子树的最大值关键字,只有叶节点存储记录数据,因此每次都会进行h次磁盘IO,但是B+树所有叶子结点都链接起来了,便于读取局部性连续数据。

二、MySQL索引的实现

在MySQL中,不同的存储引擎索引的实现方式不同。

(一)MyISAM索引的实现

  • MyISAM引擎采用了B+树作为索引结构

MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第6张图片

MyISAM的索引文件仅仅保存数据记录的主键和物理地址;

  • 在MyISAM中,主索引和辅助索引在结构上没有区别;只是主索引要求key是唯一的,而辅助索引key可以重复。

    如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:

MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第7张图片

  • MyISAM中索引检索:

    首先按照B+树搜索算法搜索索引,如果指定的key存在,则取出data域的值(相应记录的物理地址);

    然后再去数据存储区域,读取相应的数据记录。

  • MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的“聚集索引”区分

(二)InnoDB索引的实现

虽然InnoDB也使用B+树作为索引结构,但是具体的实现方式和MyISAM截然不同。

  • 区别一:InnoDB的数据文件本身就是索引文件。

    MyISAM中索引文件和数据文件是分离的,索引文件仅仅保存了数据记录的地址。

    而InnoDB中,数据文件本身就是按照B+树组织的一个索引结构;这棵树的叶节点data域保存了完整的数据记录。

    索引的key就是数据表的主键,因此InnoDB表数据文件本身就是主索引文件。

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第8张图片

    可以看到叶节点包含了完整的数据记录,这种索引叫做“聚集索引”

    因为InnoDB的数据文件本身就是按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有);如果没有显式指定,MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键。

  • 区别二:InnoDB的辅助索引data域存储相应数据记录主键的值,而不是物理地址。

    换句话说,InnoDB所有辅助索引都引用主键作为data域。

    例如,下图为定义在Col3上的一个辅助索引

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第9张图片

  • 检索:

    聚集索引,使得主键的检索十分有效;

    而辅助索引检索,需要检索两边索引,首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

了解不同存储引擎的“索引实现方式”对于“正确使用’和”优化索引“都非常有帮助;

  • 不建议使用过长的字段作为InnoDB的主键,因为所有辅助索引都引用主索引,过长的主索引会令副主索引变得过大;
  • 用非单调的字段作为主键在InnoDB中不好,因为InnoDB数据文件本省就是一颗B+树,非单调的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁分裂挑真正,十分低效,而使用自增字段作为主键则是一个很好的选择。

三、索引使用策略及优化

MySQL的优化主要分为结构优化和查询优化。此处讨论的“高性能索引策略”属于结构优化范畴。

示例数据库:

MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第10张图片

(一)索引匹配策略:最左前缀原理

联合索引的概念:MySQL的索引可以以一定顺序引用多个列,一般一个联合索引是一个有序元组,其中各个元素均为数据表的一列;

另外单列索引可以看成联合索引元素数为1的特例。

  • 以employees.titles表为例,下面先查看其上都有哪些索引:

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第11张图片

  1. 全列匹配

    image-20210824131458031

    当按照索引中所有列进行精确匹配(这里的精确匹配指“=”或“IN”匹配)时,索引可以被用到。

    理论上索引对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引。

  2. 最左前缀匹配

    image-20210824131638370

    使用了索引,但是key_len为4,说明只使用到了索引的第一列前缀

  3. 查询条件用到了索引中列的精确匹配,但是中间某个条件未提供

    image-20210824131826930

    后面的from_data虽然也在索引中,但是由于title不存在而无法与左前缀链接。

    可以手工补充中间缺失列,如果中间列元素较少且已知:

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第12张图片

  4. 查询条件没有指定索引第一列

    image-20210824132253184

  5. 匹配某列的前缀字符串

    image-20210824132513581

  6. 范围查询

    image-20210824132817550

    范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。

    同时索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第13张图片

    explain无法区分范围索引和多值匹配,用“between”并不意味之范围查询,相当于多值匹配“IN”

    MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第14张图片

  7. 查询条件中含有函数或表达式

    此时MySQL不会为这列使用索引

    image-20210824133414590

    image-20210824133447886

(二)是否为字段建立索引、建立索引的优化策略

  • 问题:索引可以查询速度,但是也有代价:索引文件本身要消耗存储空间;同时索引会加重插入、删除、和修改记录时的负担、另外,MySQL在运行时也要消耗资源维护索引;

    因此,索引并不是越多越好,一般两种情况下不建议使用索引。

    1. 表记录较少时,没必要建立索引;

      让查询做全表扫描就好,阈值在2000左右

    2. 当索引的选择性较低时,不建议建立索引;

      索引的选择性:指不重复的索引值与表记录数的比值。

      当重复键值太多,索引价值不大。

  • 为列建立索引时的一个优化策略:前缀索引——用列的前缀替代整个列作为索引的key。

    当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引;同时因为索引key变短而减少了索引文件的大小和维护开销。

(三)InnoDB的主键选择、与插入优化

  1. 主键选择

    在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。

  2. 与插入优化

    • InnoDB使用聚集索引,数据记录本身被存在主索引(一个B+树)的叶子结点上。

      这就要求同一个叶子结点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放;

      因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点及其位置中;如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)

    • 如果表使用自增主键,那么每次插入新的记录,记录就会在当前索引叶子节点的最后面添加,当一页写满,就会自动开辟一个新的页。

      MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第15张图片

      这样就会形成一个紧凑的索引结构,近似顺序填满。

      由于每次插入也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。

    • 如果表使用非自增主键(如身份证号或学号),由于每次插入主键的值近似于随机,因此每次新纪录都要被插入到现有索引页的中间某个位置:

      MySQL索引详解(索引的本质 B树B+树、MyISAM和InnoDB索引实现 非聚集索引和聚集索引、索引使用策略及优化)_第16张图片

      此时MySQL不得不为了将新记录插入到合适位置而移动数据。

      甚至目标页面可能已经被写到磁盘上而从缓存中清理掉,此时又要从磁盘上读回来,这增加了很多开销;同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

      因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

你可能感兴趣的:(mysql,索引)