本篇依然可以使用 explain 工具,分析 SQL 执行实际使用的索引。
联合索引的最左匹配原则,非常重要的原则:
MySQL 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 where a = 3 and b = 4 and c < 5 and d = 6,如果建立 (a、b、c、d) 顺序的联合索引,d 是用不到联合索引的,如果建立 (a、b、d、c) 顺序的联合索引则都可以用到,a、b、d 的顺序可以任意调整。
= 和 in 可以乱序,比如 where a = 1 and b = 2 and c = 3 建立 (a、b、c) 联合索引可以任意顺序,MySQL 的查询优化器会帮你优化成索引可以识别的格式。
如果建立 (a、b) 顺序的联合索引,where a = 1、where a = 1 and b =2 都会走联合索引,而 where b = 2 就不会走联合索引,而会走全表扫描。
索引是建立的越多越好吗?
- 数据量小的表不需要建立索引,建立会增加额外的索引开销;
- 索引会增加写操作的成本,索引越多,修改数据所需要的时间越长;
- 太多的索引会增加查询优化器的选择时间;
- 更多的索引也意味着需要更多的空间;
索引对于数据库的影响非常关键,索引的主要作用就是告诉存储引擎如何快速的找到需要的数据。当表中的数据比较少时,索引的作用可能还不是很明显,因为此时表中的数据基本上可以完全缓存在内存中,就算进行全表扫描也不会太慢。而随着表中数据越来越多,查询频率也越来越高,内存已经不能完全缓存所有数据的时候,索引的作用就会显得越来越重要。
为什么要使用索引?
- 索引大大减少了存储引擎需要扫描的数据量。以 InnoDB 来说,发生一次 IO,最小的存储单位是以页为单位的,所以一页内存储的信息越多,那么读取效率也就越快,默认情况下,InnoDB 一页的大小为 16k,由于索引的大小比数据要小的多,所以一页内可以存储更多的索引,因此通过索引查找所需要读取的页非常少,减少了存储引擎需要扫描数据的数据量,加快了查找速度;
- 索引可以帮助我们进行排序以避免使用临时表;
- 数据行的物理地址通常是随机分布的,采用索引进行查找,可以把随机I/O变为顺序I/O,可以更加充分的发挥磁盘的 I/O 性能。
索引是不是越多越好?
- 索引会增加写操作的成本,索引越多,修改数据所需要的时间越长;
- 太多的索引会增加查询优化器的选择时间;
MySQL 的索引是在存储引擎层实现的,而不是在 MySQL 的服务器层实现的,这也就决定了不同存储引擎上的索引可能用的方式是不同的。同是也不是所有的存储引擎都支持所有的索引类型。即使是同一种类型的索引,在不同的存储引擎上其底层的实现也可能不相同。
通常索引的数据结构是 B+ 树,除此之外还有 Hash 结构。
二叉查找树
二叉查找树的查找用的是二分查找。时间复杂度 O(logn)。
优点:查询效率非常高;
缺点:有可能变成线性查找树,时间复杂度变为 O(n)。
B-tree 的特征:
- 根节点包括 2 个孩子;
- 树中每个节点最多含有 m 个孩子(m >= 2);
- 除根节点和叶节点外,其他每个节点至少有 ceil(m/2) 个孩子;
- 所有叶子节点都位于同一层。
相比于二叉查找树、B 树,B+ 树更适合用来做存储索引,原因:
- B+ 树的磁盘读写代价更低;
- B+ 树的查询效率更加稳定;
- B+ 树更有利于对数据库的扫描,B+ 树只需要遍历叶子节点即可实现对全部关键字的扫描。
B-tree 索引以 B+ 树的结构存储数据,在 B+ 树中,每一个叶子节点都包含一个指向下一个叶子节点的指针,这样方便叶子节点之间的遍历。下面看一下 B+ 树的存储结构:
从图中可以看出,B+ 树是一种平衡的查找树,每一个叶子节点到根节点的距离都是相同的,并且所有的记录节点都是按照键值的大小来顺序存放到同一层叶子节点上的,并且各个叶子节点是由指针进行连接的,这样做的好处是方便进行快速查找。对于不同的存储引擎来说,具体的实现可能不同,比如 InnoDB 叶子节点指向的是主键,MyISAM 则是指向了物理地址。
下面看一下 B-tree 索引的特点:
- B-tree 索引能够加快数据的查询速度。通常情况下,索引的大小远小于表中数据的大小,使用了 B-tree 索引后,存储引擎就不再需要全表扫描来获取需要的数据了,取而代之的是从索引的根节点开始搜索,在索引根节点存储了指向下一层子节点的指针,存储引擎根据这些指针向下一层进行查找,最终存储引擎就可以根据 B-tree 索引找到符合要求的叶子节点;
- B-tree 索引更适合进行范围查找。因为 B-tree 索引对索引是顺序存储的。
在什么情况下可以用到 B-tree 索引?
- 全值匹配的查询,例如 where order_sn = ‘10001’;
- 匹配最左前缀的查询;
- 匹配列前缀查询,例如 where order_sn like ‘10%’;
- 匹配范围值的查询,例如 where order_sn > ‘10001’ and order_sn < ‘90001’;
- 精确匹配左前列并范围匹配另外一列;
- 只访问索引的查询;
B-tree 索引的使用限制?
- 如果不是按照索引最左列开始查找,则无法使用索引;
- 使用索引时不能跳过索引中的列;
- Not in 和 < > 操作无法使用索引;
- 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引;
InnoDB 除了支持 BTree 索引,还支持 Hash 索引。Hash 索引性能理论上要高于 BTree 索引。
下面看一下 Hash 索引的特点:
- Hash 索引是基于 Hash 表实现的,只有查询条件精确匹配 Hash 索引中的所有列时,才能够使用到 Hash 索引,也就是说 Hash 索引只能用到等值查询中,范围查询和模糊查询就不能使用 Hash 索引;
- 对于 Hash 索引中的所有列,存储引擎都会为每一行计算一个 Hash 码,Hash 索引中存储的就是 Hash 码,这也就是 Hash 索引只能进行全值匹配查询的原因,因为只有这样,Hash 码才能匹配,同是在 Hash 索引的表中还保存了每一个 Hash 索引所代表数据行的指针,由于 Hash 索引只存储了键值和 Hash 码以及对应行的指针,索引中并没有保存字段的值,所以 Hash 索引的存储结构是十分紧凑的,使得 Hash 索引找到数据的速度非常快。
Hash 索引的缺点?
- 仅仅能满足 “=”,“IN”,不能使用范围查找;
- 使用 Hash 索引查找数据必须进行二次查找,但是这一点对性能影响非常小;
- 无法用于排序;
- 不能利用部分索引键查询;
- 不能避免表扫描;
- Hash 索引中 Hash 码的计算可能存在 Hash 冲突,例如不能在姓名上使用 Hash 索引;
正确的创建和使用索引是实现数据库高性能查询的基础。我们在进行索引优化时,关注的重点通常是 where 语句中的过滤条件。
例如对索引上的日期列进行计算:
错误范例:
to_days(out_date) 是函数,out_date 是索引。
正确范例:
经过这样的修改,out_date 列上就不会使用函数了,这时就能正确使用到该列上的索引了。
MySQL 支持对字符串的前缀建立索引,这样可以大大减少索引的空间,从而提高索引的查询效率:
在 create index 中指定索引宽度,对于 InnoDB,索引的最大宽度是 767 个字节,使用 UTF-8 编码的话,算成字符大概是 255 个字符,对于 MyISAM 表,索引的最大宽度是 1000 个字节。如果超了这个最大宽度限制,是无法建立前缀索引的。虽然我们可以在大部分列上使用前缀索引增加索引效率,但是前缀索引降低了索引的选择性。
索引的选择性是不重复的索引值和表的记录数的比值,索引的唯一性越高,其选择性越高,唯一索引选择性最高。由于前缀索引只是取了字符串数据的一部分来作为索引的键值,所以其选择性必然降低。
如果我们想在一个很长的字符串上进行查找,就只能使用前缀索引,但是会使选择性变得很差。其实对于支持 Hash 索引的存储引擎,使用 Hash 索引会是更好的方式。
联合索引是由多个字段组成的索引。
联合索引中索引列的顺序尤为重要,那么如何选择索引列的顺序呢?
- 经常会被使用到的列优先,即放到联合索引的最左边;
- 选择性高的列优先;
- 宽度小的列优先;
覆盖索引是 select 的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。
优点:
- 可以优化缓存,减少磁盘 IO 操作;
- 由于 B-tree 索引是按照索引的键值进行存储的,所以对于 IO 密集型的范围查找来说,可以减少随机 IO,变随机 IO 操作变为顺序 IO 操作;
- 对于 InnoDB 存储引擎,可以避免对 InnoDB 主键索引的二次查询。通常情况下,通过索引查找到相应键值后,还需要通过主键索引二次查询才能获取到数据行,而在覆盖索引中,可以通过索引获取全部的数据,避免二次查询,减少相关 IO 操作;
- 对于 MyISAM 存储引擎,可以避免 MyISAM 表进行系统调用。对于 MyISAM 来说,只会缓存索引信息,数据则依赖于操作系统来缓存,因此我们访问数据需要进行一次系统调用,而系统调用的性能通常会比较差,而在覆盖索引中,可以通过索引获取全部的数据,避免系统调用的产生。覆盖索引对于优化 MyISAM 存储引擎更加的有效;
无法使用覆盖索引的情况:
- 并不是所有的存储引擎都支持覆盖索引,也不是所有的索引类型都能建立覆盖索引,只有在索引的叶子节点中,包括了键值的索引才能建立覆盖索引(Hash 索引就不能作为覆盖索引来使用);
- 查询中包含了太多的列,也不适合建立覆盖索引;
- 使用了双 % 号的 like 查询无法使用覆盖索引,这是由于 MySQL 底层的 API 所限制的;
MySQL 有两种方式生成有序的结果:
- 通过排序操作;
- 按照索引顺序扫描数据。
MySQL 使用索引扫描来优化排序需要满足两个条件:
- 索引的列顺序和 order by 子句的顺序完全一致;
- 索引中所有列的方向(升序、降序)和 order by 子句完全一致;
- order by 中的字段全部在关联表中的第一张表中;
例如在 InnoDB 中:
InnoDB 存储引擎使用的是行级锁,可以通过索引减少锁定的行数,通过索引在存储引擎层过滤掉我们不需要的行,减少锁带来的开销,来优化性能。索引可以加快数据的处理速度,同时也加快了锁的释放。
1、删除重复和冗余的索引。
如何知道哪些索引是重复或冗余的呢?
可以通过 pt-duplicate-key-checker h=127.0.0.1 工具进行检查。
2、查找未被使用过的索引。
3、更新索引统计信息及减少索引碎片。
可以通过 SQL 语句 analyze table table_name 重新生成表的统计信息。不同存储引擎生成和保存统计信息的方式不太相同,所以运行 analyze table table_name 的成本也不太一样。
对于 MyISAM 表来说,索引的统计信息会存储在磁盘中,运行 analyze table table_name 需要进行一次全索引扫描,以重新计算索引的统计信息,在这个过程中,需要对表进行锁定。
对于 InnoDB 表来说,不会在磁盘存储索引的统计信息,而是通过随机的索引访问的方式进行评估,并将其存储在内存中,所以 InnoDB 表执行 analyze table table_name 效率会很高,但是生成的统计信息不会十分准确,只是一个估算值。另外在 B-tree 索引更新的时候可能会产生大量的碎片,降低查询效率。
维护索引碎片可以通过SQL 语句 optimize table table_name 来维护,使用不当可能会导致锁表。