一、索引基础
索引是存储引擎用于快速找到记录的一种数据结构。(可以理解为一本书的目录)。
例子:where id=1 如果在id列上建有索引,则MySQL将使用该索引找到id为1的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。创建一个包含两列的索引,和创建两个只包含一列的索引是大不相同的。
1、索引的类型
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以并没有统一的索引标准:不同存储引擎的索引的工作方式不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不相同。
B-Tree
存储引擎以不同的方式使用B-Tree索引,性能也各有不同。MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引行,而InnoDB则根据主键引用被索引的行。InnoDB使用的是B+Tree。
B-Tree工作方式:在访问数据时,存储引擎不再进行全表扫描,而是从索引的根节点开始进行搜索。根节点的槽中存放了子节点的指针,存储引擎根据这些指针向下查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上下限。
B-Tree的查询类型。全键值、键值范围、键前缀。
解释中的例子:表:last_name、first_name、brithy_day。并创建组合索引key(last_name、first_name、brithy_day)。下列为组合索引图解,每个节点的第一层到第三层分别对应last_name、first_name、brithy_day
索引限制
- 如果不是按照索引的最左列开始查找,则无法使用索引。上述例子中不能直接在条件中查找first_name或brithy_day,因为此类查找跳过了最左边的索引last_name。
- 不能跳过索引中的列。条件查询last_name和brithy_day,跳过了first_name,则生效的索引列只有last_name。
- 如果条件中存在范围查询,则范围查询后的列索引失效。条件查询“last_name=abc and first_name like "%bb% " and brithy_day ="2020-01-11" ”,索引生效的列只有last_name和first_name
哈希索引
哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
在MySQL中只有Memory引擎支持哈希索引。这也是Memory引擎表的默认索引类型,同时它也支持B-Tree索引。Memory引擎如果存在多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。
哈希索引的数据结构有两列:槽、值,槽为对应索引列计算出的哈希值,值为数据所在行的指针。
MySQL中查询带有哈希索引列时,首先会读对传入的条件值进行哈希计算得到一个哈希值,然后去匹配哈希索引中所在的槽,并得到值(存储的是指向所在行的指针)。
哈希索引的限制:
- 哈希索引只包含哈希值和行指针,而不是存储字段的值。不过访问内存中行速度快,并不影响性能
- 哈希索引数据并不是按照索引值的顺序存储,所以不能用于排序。
- 哈希索引不支持匹配查询。因为哈希值是用列的全部内容计算得出的值。
- 哈希索引只支持等值匹配(=、in、<>...),不支持范围查询(>、<...)
- 哈希索引访问速度快,除非出现哈希冲突(不同内容计算出相同的哈希值),当出现哈希冲突时,存储引擎必须便利链表中的所有元素与之匹配。
- 哈希冲突多的话,维护操作的代价较高。当建立哈希索引的列中出现很多哈希冲突,就会导致此哈希值链表很长,比较对应的值就会便利此链表。
InnoDB引擎中有一个特殊的功能叫做“自适应哈希索引”。当InnoDB注意到某些索引值被使用的非常频繁时,它会在内存中基于B-Tree索引之上在创建一个Hash索引,这样B-Tree就有了哈希索引的一些优点。
伪哈希索引。对于URL这类较长的字符,使用B-Tree会导致存储的内容比较大。可以针对存储URL字段的列在创建一列,用于保存URL进行哈希计算的值,在进行where查询时可以直接使用这个字段去匹配条件值的Hash值。建议使用CRC32做哈希计算,SHA1和MD5计算的哈希值较长,会浪费大量空间。同时也可以使用插件来引入FNV64函数,相较于CRC32,速度快,且有更长的哈希值,减少哈希冲突。
二、索引的优点
1、减少服务器需要扫描的数据量
2、帮助服务器避免排序和临时表
3、可以将随机I/O变为顺序I/O
索引对于中大型表比较有效。小型表可以直接做全表扫描,特大型表就需要分区分库等技术。
三、高性能策略
1、独立的列
MySQL在使用索引列作为查询条件时,不正确的使用方式会使得索引失效。”独立的列“是指索引列不能是表达式的一部分,也不能是函数的参数。
错误的列子:
where id+1=3;
where YEAR(create_time)=2023
2、前缀索引和索引选择性
对很长的字符列建立索引,会使索引变得大且慢。对于BLOB、TEXT、VARCHAR这些列则必须使用前缀索引,因为MySQL也不允许索引这些列完整的长度。
通常针对这些长字符列可以索引开始部分字符,这样可以减少存储空间的同时提高索引效率,但也会降低索引选择性。索引选择性越高查询效率就越高,因为选择性高的索引能过滤越多的数据行。选择性为1,性能则是最好的。
索引选择性:不重复的索引值和数据表的记录总数的比值,范围从1/数据总数到1之间。
在索引效率和选择性之间做出合适的选择,就需要选择合适的前缀长度。下列SQL可以帮助选择合适的前缀长度。
select count(*) as num,left(列,长度) as pref
from 表
group by pref
解释:num的值越小代表选择性越高(越能过滤掉非目标数据),长度越短越效率越高(存储空间越小)
找到合适的长度后就可以建立对应的索引列
alter table 表名 add key(列(长度))
前缀索引是一种能使索引更小、更快的有效方式,但是也有缺点:MySQL无法使用前缀索引进行ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
3、多列索引
在多个列上建立独立的索引大部分情况下并不会提高MySQL的查询性能。MySQL5.0及以上版本引入了一种”索引合并“策略,一定程度上可以使用表上的多个单列索引来定位指定的行。
在MySQL5.0及以上中,查询能够同时使用多个单列索引进行扫描,并将结果进行合并。这种算法有三种:OR(unionn),AND(intersection,组合(or和and的联合及相交)。
验证SQL是否进行了索引合并,可以使用下列例子在结果中查看。
EXPLAIN SQL语句
在结果中Extra行可以查看是否使用到了联合索引。
索引合并策略有时候是一种优化的结果,但是上更多时候说明了表上的索引建的很糟糕
- 当出现服务器对多个索引做相交操作时(通常有个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引
- 当服务器需要对多个索引做联合操作时(通常由多个OR条件),通常需要耗费大量CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。
- 更重要的是,优化器不会把这些计算到”查询成本“中,优化器只关心随机页面读取。这会使得查询的成本被”低估“,导致该执行计划还不如直接走全表扫描。
4、选择合适的索引列顺序
1、创建组合索引的时候将选择性高的列放在前面。
select * from user where type=1 and city="LA"
针对上述例子建立组合索引时,是建立key(type,city),还是key(city,type)。在确定顺序之前可以看看那个字段的选择性比较高。可以通过”select sum(type=1),sum(city="LA" from user)“来查看两个字段分别在表中有多少,越少的选择性越高,选择性越高索引位置越靠前。但是按照这种办法需要注意,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,这需要根据实际情况做定夺。
5、聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一结构中保存了B-Tree索引和数据行,(B+Tree)。
当表有聚簇索引时,数据行实际存放在索引的叶子页中。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。下图为聚簇索引实例。
对于聚簇索引InnoDB将通过主键聚集数据,如果没有主键则会选择一个唯一的非空索引代替。如果没有这种索引,InnoDB会隐式定义一个主键来作为聚簇索引。
聚簇索引的优缺点:
优点:
- 可以把相关数据保存在一起。例如实现邮箱,可以根据用户ID来聚集数据,读取很少的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则查询时可能会导致每封邮件一次I/O
- 数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引获取数据通常比非聚簇索引快。
- 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
缺点:
- 聚簇数据最大限度提高I/O密集型应用的性能,但是数据全部放在内存中,则访问的顺序就没有那么重要,聚簇索引就没有太大的优势了。
- 插入速度严重依赖插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
- 更新聚簇索引的代价很高,会强制InnoBD将每个被更新的行移动到新的位置。
- 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行。页分裂会导致表占用更多的磁盘空间。
- 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
- 二级索引(非聚簇索引)可能会更大,因为二级索引的叶子节点包含了引用行的主键。
- 二级索引访问需要两次索引查找。(二级索引叶子节点保存的是行的主键,不是指向行物理位置的指针。通过二级索引查找行,首先找到主键,在通过主键查找聚簇索引中主键位置对应的物理位置指针,从而查找目标行。)
6、InnoDB和MyISAM的数据分布对比
MyISAM
他是按照数据插入顺序存储在磁盘上的。他会在表中提供一个隐藏列用于保存数据行的行号,在索引的数据结构中,每个节点都包含了索引列的值以及对应数据行的行号。(行号只针对于定长的表,非定长的表还有其他策略)
不管是主键还是其他索引列(所有索引),列中的内容都是索引列的值+行号
InnoDB
InnoBD中的聚簇索引的叶子节点中包含了主键列、事务ID、回滚指针、非事务列。非聚簇索引即二级索引的节点(可以是叶子节点、也可以是非叶子节点)的组成:索引列、主键列。
InnoDB中主键的顺序问题
对于主键(聚簇索引)建议使用自增列,而不使用UUID之类的随机数。
采用UUID出现的问题:耗时更长、存储空间消耗的更大。
对于顺序主键,在插入数据时会直接存储到上一条数据后面。当达到页最大填充因子时(默认15/16,剩余空间用于后期修改),下一条记录就会写入新的页中。
对于随机主键,新的主键不一定比之前的大,所以可能需要更改以前填充满的页,就会涉及到分页、移动数据等操作。
随机主键的缺点:
- 写入的目标页可能已经刷到磁盘上并从缓存移除,或者还没被加载到内存中。InnoDB在插入数据前找到并从磁盘读取目标页到内存中。这会导致大量I/O操作。
- 因为写入是乱序的,InnoDB不得不频繁的做页分裂操作,以便于新的行分配空间。页分裂会导致移动大量的数据,一次插入最少需要修改三个页不是一个页。
- 由于 频繁的页分裂,页会变得稀疏并被不规则填充,最终数据会有碎片。
顺序主键的缺点:
对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键的上界会成为”热点“。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。
7、覆盖索引
概念:一个索引包含所有需要查询的字段的值。
如果查询只需要扫描索引而无需回表带来的好处:
(回表:二级索引查询到对应主键,在利用主键去查询主键索引中对应数据)
- 索引条目通常远小于数据行大小,减少数据访问量、减少缓存负载、减少数据拷贝时间
- 索引是按照顺序存储,对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O少。对于MyISAM这一类的表可以使用OPTIMIZE命令优化索引。
- 一些存储引擎只在内存中缓存索引,数据则以来操作系统缓存,因此访问数据需要一次系统调用。这可能导致严重的性能问题,尤其是系统调用占数据访问中最大开销的场景。
- InnoDB的二级索引在叶子节点保存了行的主键,所以二级索引能够覆盖查询,则可以避免对主键索引的二次查询。
不是所有类型的索引都可以成为覆盖索引。覆盖索引必须存储索引列的值,而哈希索引、空间索引和全文索引等都不能存储索引列的值,所以MySQL中只能使用B-Tree索引做覆盖索引。
当执行覆盖索引的查询时,可以看到EXPLAIN的Extra列为Using index。
索引无法覆盖查询的原因:
- 没有任何索引能够覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。
- MySQL不能在索引执行Like操作。在MySQL5.5及以前版本只允许索引做等于、不等、大于等操作。MySQL能在索引中做最左前缀匹配的Like比较,因为该操作可以转换为简单的比较操作,但如果以通配符开头的like查询,存储引擎就无法做比较匹配。
解决上述两个问题的办法
select *
from user
inner join (select id from user where type=1 and name like "abc%") a
on a.id=user.id
解释:type、id、name三个字段为索引列。这种方式叫做延时关联,因为延时了对列的访问时间。在查询的第一阶段MySQL可以使用覆盖索引,在from子句中的子查询中匹配id,然后根据id去外层查询匹配获取需要的所有列的值。
8、索引扫描来做排序
MySQL有两种方式可以生成有序结果:通过排序、按照索引顺序扫描。如果explain出来的type列的值为“index”,则说明MySQL使用索引扫描来排序。
扫描索引本身就很快,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就要回表一次对应的行。这基本都是随机I/O,因此按照索引顺序读取数据的速度通常比顺序地全表扫描慢,尤其是在I/O密集型的工作负载时。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL才能使用索引对结果排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一张表时,才能使用索引排序。ORDER BY和查询的限制是一样的:需要满足索引的最左前缀的要求。
有种情况可以不用满足最左前缀原则:where和order by后面的条件字段加起来满足最左前缀原则。这样即使order by子句不满足最左前缀的要求,也可以用于查询排序。例子:
select type,name from user where type=2 order by name
解释:user表中有组合索引,其顺序为:type,name。则这种情况下也会走索引排序。
下列例子是一些不能做索引排序的查询(假设下列例子中的有组合索引(type,name,num)):
- 查询使用了两种不同的排序方向
select * from user where type=2 order by name desc , num asc;- order by 中引用了不在索引中的列
select * from user where type=2 order by name desc , create_time asc;- where和order by中的条件无法组成最左前缀原则
select * from user where type=2 order by num;- 查询索引列存在范围查询
select * from user where type>2 order by num;
select * from user where type=2 and name in(1,2) order by num;
四、索引案例
1、建立组合索引可以把查询率高的字段放前面,不管他的选择性高低。
查询率较高,就算需要过滤这个字段,也可以使用通过 “in (男,女)”这种方式,容易满足最左前缀原则。但是in的值太多,这种方式就不适用。
2、容易进行范围查询的字段放到后面去。
为了尽可能使用更多的列进行索引,而查询只能使用最左前缀,直到遇到第一个范围查询条件列。比如日期这一字段,一般查询就是在一个日期到另一个日期之间。
3、避免多个范围条件
范围条件可以使用explain查看,type列为range类型则是范围查询。
where age>2where age in (1,2,3)
上述两个例子都是范围查询。但是第一个例子会是索引失效,而第二个不会,因为第二个是“多个等值条件查询”
MySQL的索引无法进行两个及以上的范围查询,所以在范围查询时可以使用in代替(in的值不建议太多)。
五、维护索引和表
维护表的目的:找到并修复损坏的表,维护准确的索引信息,减少碎片。
1、找到并修复损坏的表
MyISAM存储引擎
表损坏通常是系统崩溃导致的。也可能是硬件、MySQL设计缺陷、操作系统内的问题导致索引损坏。
损坏的索引会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时还会导致数据库的崩溃。可以使用check table(有些存储引擎不支持)来检查是否发生了表损坏。
找出错误后可以使用repair table 命令来修复损坏的表,但不是所有存储引擎支持该命令。
InnoDB
对于InnoDB不支持上述命令修复,可以使用alter talbe命令来重建表
alter table user engine=innodb
但是对于InnoDB来说,出现了损坏,那一定发生了严重的错误,需要找到损坏的原因。如果出现损坏,可能是内存或者磁盘的问题,也可能是在MySQL外部操作了数据文件。
2、减少索引和数据碎片
B-Tree索引可能会碎片化,这回降低查询的效率。碎片化的索引可能会以很差或无序的方式存储在磁盘上。
一下是三种数据碎片:
- 行碎片
这种碎片指的是数据行被存储为多个地方的多个片段中。即使查询只从索引中访问一行记录,行碎片也会导致性能下降。- 行间碎片
逻辑上顺序的页,或者行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操作有很大的影响,因为这些操作原本能够从磁盘上顺序存储的数据中获益。- 剩余空间碎片
数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据。
对于MyISAM表这三类碎片化都可能发生。但InnoDB不会出现短小的行碎片,InnoDB会移动短小的行并重写到一个片段中。
可以通过执行optimize table或者导出再导入的方式来重新整理数据。对于不支持optimize table的存储引擎,可以通过一个不做任何操作的alter table来重建表。