MySQL索引及常见面试题

一、索引是什么?

索引(Index)是帮助 MySQL 高效获取数据的数据结构,是对表中一列或多列值进行排序的结构。
就比如索引是一本书的目录,可以通过目录快速查找自己想要查询的东西。

二、索引为什么使用B+树?

先看一下常见的索引存储结构

  1. 哈希表 是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后 value 放在数组的这个位置。
    MySQL索引及常见面试题_第1张图片

    1. 哈希表这种结构适用于只有等值查询的场景
    2. 有序数组索引只适用于静态存储引擎,插入数据时必须得挪动后面的数据,成本太高.
  2. 二叉搜索树,每个节点的左儿子小于父节点,父节点又小于右儿子。二叉树做为索引就直接变成链表查找了
    查找的时间复杂度是 O(log(N))。

    • 任意节点左子树不为空,则左子树的值均小于根节点的值
    • 任意节点右子树不为空,则右子树的值均大于于根节点的值
    • 任意节点的左右子树也分别是二叉查找树
    • 没有键值相等的节点
  3. 平衡二叉树

    • AVL 树是严格的平衡二叉树,所有节点的左右子树高度差不能超过1;AVL树查找、插入和删除在平均和最坏的情况下都是O(logn).

    • 插入和删除可能破坏二叉树的平衡,此时需要通过一次或多次树旋转来重新平衡这棵树。当插入数据时,最多只需要1次旋转(单旋转或双旋转);但是当删除数据时,会导致树失衡,AVL需要维护从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级为O(lgn)。

    • 但由于旋转的耗时,AVL树在删除数据时效率很低。 在删除操作较多时,维护平衡所需的代码可能高于其带来的好处,因此AVL实际应用并不广泛。

  4. 红黑树
    红黑树相对于 AVL 树 只是确保从根到叶子的最长的可能路径不多于 最短的可能路径的两倍长

    • 节点是红色或黑色
    • 根节点是黑色
    • 所有叶子是黑色的
    • 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)
    • 从任一节点到每个结点的所有简单路径都包含相同数目的黑色结点

    对于数据在磁盘等辅助存储设备中的情况(如Mysql等数据库),红黑树还是并不擅长,因为红黑树还是有点高。因为当数据在磁盘中,磁盘 IO 会成为最大的性能瓶颈,设计的目标应该是尽量减少IO次数;而树的高度越高,增删改查所需要的 IO 次数也越多,会严重影响性能

  5. “N 叉”树,N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。
    以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。

B- 树与 B+ 树的区别
  1. B- 树
    B 树又称为 B- 树,是一种平衡多路查找树,描述B树,一般需要指定其阶数 M,阶数指的是一个节点包含的子节点最大数量

    • 每个节点最多有 M - 1 个关键字
    • 除根节点外,其余的节点至少有 ceil(M/2)-1 个关键字(ceil为向上取整)
    • 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它
    • 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同
  2. B+ 树

    • B+ 树包含两种节点,一种是非叶子节点(还有一种叫法是内节点),一种是叶子节点
    • B+ 树与 B 树,最大的不同是 B+ 树的非叶子节点不保存数据,只用于索引,所有数据都保存在叶子节点
    • 非叶子节点最多有 M - 1 个关键字,阶数 M 同时限制了叶子节点最多存储 M - 1 个记录
    • 索引节点中的 key 都按照从小到大的顺序排列,对于内部节点中的一个 key,左子树中的所有 key 都小于它,右子树中的 key 都大于等于它。叶子节点中的记录也按照 key 的大小排列
    • 每个叶子节点都存有相邻叶子节点的指针,叶子节点本身依关键字的大小从小到大顺序连接(范围查找特性)
  3. B树与 B+ 树的区别

    1. B+ 树的层级更少B+ 树非叶子节点上是不存储数据的,仅存储键值,叶子结点上存储数据,而 B 树节点中不仅存储键值,也会存储数据。所以一层中 B树可存储的数据就少;同时层级少了磁盘 IO 就少了查询的速度也就快了
      • B- 树查找到节点就可以返回数据,B+树如果是通过二级索引还需要回表查询(覆盖索引可以直接返回)
    2. B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时更方便,数据紧密型很高,缓存的命中率也会比B树高。
    3. B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历
存储结构总结
  1. 二叉查找树(BST):解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表
  2. 平衡二叉树(AVL):通过旋转解决了平衡的问题,但是旋转操作效率太低
  3. 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多
  4. B树:通过将二叉树改为多路平衡查找树,解决了树过高的问题,但非叶子结点存储数据层级仍然不低
  5. B+树
    • 在B树的基础上,将非叶节点改造为不存储数据的索引节点,进一步降低了树的高度
    • 此外将叶节点使用指针连接成链表,范围查询更加高效
索引的文件存储
  • 存储引擎为 MyISAM

    • *.frm:与表相关的元数据信息都存放在 frm 文件,包括表结构的定义信息等
    • *.MYD:用于存储 MyISAM 表的数据
    • *.MYI:用于存储 MyISAM 表的索引相关信息
  • 存储引擎为InnoDB
    *.frm:与表相关的元数据信息都存放在 frm 文件,包括表结构的定义信息等
    *.ibd:表数据和索引的文件。该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据

三、索引的优缺点

  1. 优点

    • 索引大大减小了服务器需要扫描的数据量,从而大大加快数据的检索速度
    • 索引可以帮助服务器避免排序和临时表
    • 索引可以将随机 IO 变成顺序 IO
    • 索引对于 InnoDB(对索引支持行级锁)非常重要,因为它可以让查询锁更少的元组
    • 关于InnoDB、索引和锁:InnoDB在二级索引上使用共享锁(读锁),但访问主键索引需要排他锁(写锁)
    • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性
    • 可以加速表和表之间的连接
    • 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间
    • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
  2. 缺点

    • 索引虽然提高了查询速度,但是随着数据的增加会增加维护和新建索引的耗时,如对表进行 INSERT、UPDATE 和 DELETE。因为更新表时,MySQL 不仅要保存数据,还要更新保存索引文件
    • 建立索引会占用磁盘物理空间。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件会变得非常大
    • 如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果
    • 对于非常小的表,大部分情况下简单的全表扫描更高效

四、索引的创建规则

  • 应该创建索引的列
    1. 在经常需要查询的列上,可以加快搜索的速度
    2. 在主键的列上,强制该列的唯一性和组织表中数据的排列结构
    3. 在经常用在连接(JOIN)的列上,这些列主要是一外键,可以加快连接的速度
    4. 在经常需要根据范围(<,<=,=,>,>=,BETWEEN,IN)进行查询的列上创建索引,因为索引是有序的,其指定的范围是连续的
    5. 在经常需要排序(order by)的列上创建索引,因为索引是有序的,这样查询可以利用索引的有序性,不需要再进行排序直接可以返回
  • 不应该创建索引的列
    1. 对于那些在查询中很少使用的列不应该创建索引
      若列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
    2. 对于那些只有很少数据值或者重复值多的列也不应该增加索引,也就是基数小的列不要建索引
      这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
    3. 对于那些定义为 text, imag e和 bit 数据类型的列不应该增加索引
      这些列的数据量要么相当大,要么取值很少,定义前缀索引区分度也不高
    4. 对修改频率高的列不要建索引,因为带来的查询效果已经远远低于频繁更新带来的维护索引的消耗

五、索引分类

  1. 从应用功能上分

    1. 主键索引
      一张表只能有一个主键索引,不允许重复、不允许为 NULL

      ALTER TABLE TableName ADD PRIMARY KEY(column); 
      
    2. 唯一索引
      数据列不允许重复,允许为 NULL 值,一张表可有多个唯一索引,索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一

      CREATE UNIQUE INDEX IndexName ON `TableName`(`column`(length));
      
      ALTER TABLE TableName ADD UNIQUE (column); 
      
    3. 二级索引(普通索引)
      一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许 NULL 值插入

      CREATE INDEX IndexName ON `TableName`(`column`(length));
      
      ALTER TABLE TableName ADD INDEX IndexName(`column`(length));
      
    4. 联合索引
      一个组合索引包含两个或两个以上的列。查询的时候遵循 mysql 组合索引的 “最左前缀”原则,即使用 where 时条件要按照建立索引的时候字段的排列方式放置索引才会生效。

      CREATE INDEX IndexName ON `TableName`(`column`(length), `column`(length));
      
    5. 全文索引
      它查找的是文本中的关键词,主要用于全文检索

  2. 从数据结构上区分

    1. 聚簇索引

      • 将数据存储与索引放到了一块,找到索引也就找到了数据。使用聚簇索引可以减少回表

      • 聚簇索引对于主键的排序查找和范围查找速度非常快
        InnoDB 的聚簇索引

        • InnoDB 对主键建立聚簇索引
        • 如果你不指定主键,InnoDB 会用一个具有唯一且非空值的索引来代替。
        • 如果不存在这样的索引,InnoDB 会定义一个隐藏的主键,然后对其建立聚簇索引

        由于聚簇索引是将数据跟索引结构放到一块,因此一个表仅有一个聚簇索引

        MySQL索引及常见面试题_第2张图片

    2. 非聚簇索引
      将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行。

聚簇索引和非聚簇索引对比
  1. 聚簇索引优点
    • 数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快,一次将一页的数据和索引加载到内存,找到叶子结点就找到了数据,相对于普通索引减少了回表。
    • 聚簇索引对于主键的排序查找和范围查找速度非常快,普通索引使用主键作为"指针"而不是使用地址值作为指针的好处是,减少了页分裂或页合并时对普通索引的维护,主键比指针占用更多的空间但是换来的是对索引的维护时间。
  2. 聚簇索引的缺点
    • 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键,比如使用 uuid 时存储的数据稀疏,就出现使用聚簇索引可能比扫描全表还要慢。
    • 更新主键的代价很高,因为将会导致被更新的行移动。
    • 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
为什么建议使用自增 ID 做主键?
  1. 减少页分裂:聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增 id,不断地调整数据的物理地址、分页,当然也有其他一些措施来减少这些操作,但却无法彻底避免。如果是自增的,那就简单了,它只需要一页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。

  2. 因为MyISAM的主索引并非聚簇索引,那么他的数据的物理地址必然是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,于是开始不停的寻道不停的旋转。聚簇索引则只需一次I/O。(强烈的对比)
    不过,如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。

六、索引查询

MySQL索引及常见面试题_第3张图片

InnoDB 的索引模型

左侧为主键索引树,可以看到数据存在叶子结点上,右侧为普通索引树,叶子叶子结点存储的是主键值。
主键越小,普通索引的叶子结点也就越小,普通索引占用的空间也就越小,根据普通索引查询时可以一次加载更多数据。

根据主键和普通索引查询的过程
  1. 主键查询
    在索引树上查找,先是通过 B+ 树从树根开始,按层搜索到叶子节点,然后可以认为数据页内部通过二分法来定位记录,查询到叶子结点直接返回行数据,唯一索引与主键一样,查询到数据直接返回,如果使用的不是覆盖索引同样会多一步回表查询。
  2. 普通索引
    在普通索引树上查找,先是通过 B+ 树从树根开始,按层搜索到叶子节点,然后可以认为数据页内部通过二分法来定位记录,查询到叶子结点拿到主键值,再去主键树上进行查找返回对应的行记录。
  3. 全表扫描
    从根结点开始查找,加载到内存中,判断当前行数据是否目标值,是放到结果集中,继续下一行,知道扫描全表数据完成。

通过索引树的扫描可以大大减少数据的检索量同时因为维护索引树时数据是按照顺序维护的所以可以将一个查询的随机 IO 变成顺序 IO
查询时尽量使用主键索引,没有主键时就使用普通索引或者联合索引等,一定要避免全表扫描。

索引的 join 连接查询
select * from t1 straight_join t2 on (t1.a=t2.a);

假设使用上面的语句进行查询, t2 的字段 a 上有索引。他的查询过程就是:

  1. 从表 t1 中读入一行数据 R;(此时对 t1 做的是全表扫描)
  2. 从数据行 R 中,取出 a 字段到表 t2 里去查找;
  3. 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
  4. 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

可以看到在查询 t2 时只需要根据索引树进行查询就可以了,不需要再进行全表扫描,这样减少了对 t2 的扫描。

使用索引进行查询排序的过程
select city, name, age from T where city = 'beijing' order by name limit 1000;
  1. 假设 city, name 是一个联合索引
    1. 从索引 (city,name) 找到第一个满足 city='beijing’条件的主键 id;
    2. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回;
    3. 从索引 (city,name) 取下一个记录主键 id;
    4. 重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city='beijing’条件时循环结束。
  2. 假设 city, name, age 是一个联合索引,此时满足覆盖索引
    1. 从索引 (city,name,age) 找到第一个满足 city='beijing’条件的记录,取出其中的 city、name 和 age 这三个字段的值,作为结果集的一部分直接返回;
    2. 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
    3. 重复执行步骤 2,直到查到第 1000 条记录,或者是不满足 city='beijing’条件时循环结束。

七、索引的实战

最左前缀

对于多列联合索引,索引的存储方式首先根据第一个列进行排序,第一个列相同的再根据第二个列进行排序插入,直到最后一个列,查询时首先根据第一个列进行匹配,如果直接根据第二个列进行匹配查询,他是无序的就用不上索引了,这就是最左前缀
当然一个有三个列的联合索引where条件进行三个等值查询可以不按照索引顺序进行查询,这个时候查询优化器会把三个列按照索引创建顺序进行调整。也就是覆盖索引查询时可以不按照索引列顺序进行 and 连接。

覆盖索引
select city, name, age from T where city = 'beijing' and name = "Jugg" and age = 32;

前面也提到了覆盖索引,还拿上面这个例子来说,city, name, age 三个列为联合索引,要查询的列全都在索引里面,这样就不需要回表进行查询,直接就返回数据了。
覆盖索引的查询效率少了一次回表的过程,查询的效率会比普通索引查询的效率高。

前缀索引

有时候需要索引很长的字符列,这会让索引变得大且慢。通常可以以某列开始的部分字符作为索引,这样可以大大节约索引空间,从而提高索引效率。
但这样也会降低索引的选择性。索引的选择性是指不重复的索引值和数据表的记录总数的比值,索引的选择性越高则查询效率越高。

使用前缀索引时就没办法再使用覆盖索引了,因为前缀索引不知道当前字段是否完整,需要回表取整行数据进行判断。

回表

回到主键索引树搜索的过程,我们称为回表。

普通索引存储的是索引键和主键值不能返回要筛选的全部字短,所以要回到主键树去查询整行数据进行返回。

索引下推

MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

索引失效情况
  1. 如果条件中有 or、is null、<> ,即使其中有条件带索引也不会使用,or 条件时主键可以使用索引
  2. 对于联合索引,不是使用的第一列也就是不使用最左前缀,则不会使用索引
  3. like 查询是以通配符 % 开头,这样就会放弃索引走全表扫描,但是以 % 结尾没问题可以继续使用索引
  4. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引,如果列是数字类型条件中加引号同样可以走索引查询
  5. 如果mysql估计使用全表扫描要比使用索引快,则不使用索引
  6. 对索引列进行运算,比如函数计算就会索引失效
普通索引和唯一索引的插入过程

如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的?

  1. 第一种情况是,这个记录要更新的目标页在内存中。InnoDB 的处理流程如下:

    • 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。

    这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。

  2. 第二种情况是,这个记录要更新的目标页不在内存中。InnoDB 的处理流程如下:

    • 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
索引的设计建议
  1. 索引字段尽量使用简单的数据类型,比如数字
    • 因为在查询时对字符串的比较是逐个比较,耗费性能
    • 数字比较小这样索引占用的空间会小
  2. 尽量不要让字段的默认值为 null

reference

[MySQL索引总结] (https://zhuanlan.zhihu.com/p/29118331)  
[一文搞懂MySQL索引] (https://blog.csdn.net/wangfeijiu/article/details/113409719)

你可能感兴趣的:(mysql,b树,数据结构,索引)