总结一下Mysql Innodb索引相关的知识,索引是以空间换时间的方式来加快查询速度。本质是将查询涉及的字段单独拎出来减少查询的基数和减少磁盘io次数,先做了排序可以用更高效的查询算法。
同时索引也有一些弊端,需要占用额外的空间,还不少。数据写入时,维护索引的数据排序,要消耗cpu。随着索引数量增加,查询优化器要评估每个索引的效率,对于用不到的索引的评估时间,影响查询效率。
索引分类
主键索引
primary key标注的列会被设置表的主键,可以是多个字段作为联合主键。Innodb的会按照主键顺序将记录组织成b-tree(实际是b+tree,统称为b-tree)形式。叶子结点存放记录数据,非叶子节点存放主键指作为索引的key。这棵b-tree就是主键索引。
普通索引
普通索引的结构也是b-tree,只是叶子结点存放的值不是记录,而是主键的值。所以,普通索引的使用流程是,现在索引上进行过滤,找到主键后,去主键索引里找到记录,这个过程称为回表查询。
唯一索引
唯一索引是在普通索引的基础上,加了唯一的约束,会在记录插入时,判断记录中包含的字段是否存在,存在则不允许插入。在查询的用法上跟唯一索引一样。
索引结构类型
B-tree索引
Innodb是用b+tree形式组织索引数据,b+tree是一颗多叉树,非叶子结点存放索引字段的值,叶子结点存放主键/记录。叶子结点有一个指针指向下一个叶子结点,提供范围查询的效率。
Hash索引
哈希索引将索引列以哈希表的形式存放,适合单值查询,不适合范围查询,所以使用的不多。Innodb不支持哈希索引,但是有一个自适应哈希索引的优化,会对b-tree索引上频繁访问的值,会在原有的索引之上,在内存中自动建立哈希索引,提高查询效率。
怎么使用索引
索引使用的三个原则:
- 单行访问是慢的,一次磁盘io会获取一页数据,如果查询只用到数据页里的一行数据,性价比就很低。
- 按顺序查询数据是很快的,因为有机会用到顺序io,速度很快;另外顺序查询免去了排序的消耗。
- 索引覆盖查询很快,因为不需要回表,避免了单行访问。
所以最理想的情况是,用索引查询连续的结果集。
索引的三星原则:
- 将查询相关数据放到一起
- 索引数据顺序和查询顺序一致,使order by能用到索引
- 索引的接包含查询中需要的全部列
实例要看查询优化器的结果,用explain判断索引是否生效。
索引使用方式
前缀索引
当索引的列值很大,例如对url加了索引,一个磁盘页(16kb)能存的索引记录数就越少,查询索引就需要读取更多的磁盘页,降低查询效率。
Innodb支持前缀索引,只对列的前一部分内容做索引,能显著减少索引值大小。
sql
alter table post add index idx_url(url(100));
使用前缀索引时,要保证放入索引的部分内容的选择性。选择性越高,使用索引一次判断能过滤掉的记录数就越多,查询的效果就越好。选择性的计算公式一般是:
sql
select count(distinct substr(url, 1, 10)) / count(1) from post;
-- 0.5806
多列索引
当一次查询用到多个字段时,我们可以对查询列组合起来建立一个索引,这样的索引叫多列索引或联合索引。
sql
alter table post add index idx_title_author(title, author);
多列索引在能解决单列索引的很多问题,当然它也有一些自身的问题。我们先看下单列索引有什么问题。
工作中碰到一个同事,他把表的每个字段都加上索引,说这样在任何查询至少能用到1个索引。这样的方式很简单,但往往不是最优的方式。
以select id from post where category = 'game' and hot = 100,在category和hot上都有索引。如果索引只命中了category一个索引,就需要回表查询hot=100,回表的操作是随机io,可能比顺序的全表扫描还慢。当然,mysql会使用一种索引合并的技术,使用表中多个单列索引来定位记录。这种情况下,category和hot都能击中索引。
ini
**索引合并**
索引合并支持or、and和混合的情况。使用explain能看到type=index_merge,并且能在extra里看到using union(idx_category, idx_hot)等信息。
索引合并事实上不是一个好的选择。对于and的情况出现合并索引,最好是改成多列索引。对于or的情况,在对数据进行排序和合并时,合并算法会消耗大量cpu和内存。并且查询优化器还看不到这部分的消耗,导致实际查询成本比优化器提示的成本要高很多。
所以,尽量别用索引合并的功能,可以通过optimizer_switch来关闭该功能。
索引列的顺序
多列索引对索引列要满足最左匹配原则,即左边的列先满足查询条件,才会用上下一个列。并且,前面列的选择性越好,过滤掉越多的记录,那下一个列需要过滤的基数就越少。所以,我们一般会将选择性最高的列放在最左边。
但也会有特殊的情况,例如select id from post where author_id = 1 and category = 'game'; author_id值区间是1-6,1是管理员账号,category值有5种。现在author_id 2-6的人在不同category下发了一片文章,author_id为1的管理员也在不同category下发了一片文章。这时候author_id的选择性是6/10=0.6,category的选择性是5/10=0.5,按照经验是把author_id放在多列索引的前面。而上面这条查询,先查author_id后会返回5条记录,而现查category后会返回2条记录,所以category先查会更优一点。
上面这个情况是个特例,用来说明对索引顺序的选择要结合实际情况,但系统往往是被这种特殊的情况而击垮。
另外,是否要把单列索引尽可能换成多列索引呢?
当然不是,使用多列索引是有成本的,多列索引在多值查询的时候有收益,但是对于单值查询来说,就有损耗了。因为多列索引的key会变长,并且b-tree节点数变多,会让索引的体积变大。那么单列查询的性能就降低了。
隐藏的多列索引
由于非聚簇索引的叶子节点是主键id,并且id值是有序的,所以非聚簇索引默认和id组合成了多列索引。例如单列索引idx_author(author),它实际的效果跟idx_author(author, id)是一样的,多列所有也是一样的道理。
覆盖索引
索引的列包含查询所需的所有信息时,包括select、where、order by、group by子句涉及的列,这时不需要二次回表,这样的索引称为覆盖索引。
覆盖索引拥有非常优秀的性能,在查询当击中时,explain的extra字段会包含using index值。
使用索引来排序
Mysql索引除了能用来过滤记录,还能用在order by帮助排序。当排序击中索引时,explain的type=index;未击中索引时,extra字段会保护using filesort。
使用索引进行排序有几个条件:
- 索引的顺序和order by子句的顺序完全一样,并且所有列的排序方向都一样
- 联合查询语句,order by引用的列要全部在第一张表中
- 要满足索引最左前缀的要求
当然,并不是满足条件就会使用索引排序,还要根据查询优化器的总和评估成本。例如,排序完还需要回表查询其他字段,当回表次数多时,就不会使用索引信息,直接通过全表扫描后再排序。
索引下推
在mysql5.6开始,在索引遍历的过程中,优先用索引的字段进行过滤,减少回表次数。例如查询语句a like 'x%' and b = 'y' and c = 'z'的时候,联合索引(a,b)根据最左匹配原则只能用到索引a字段,然后就需要回表查询到b字段再进行比较。有了索引下推,就能优先使用索引里的b字段进行过滤,然后再去回表查询。
聚簇索引
Innodb里,数据是随着主键存储,所以主键索引就是聚簇索引。在未设置主键时,系统会默认添加一个6字节的自增主键。这个自增锁是全局公用,所以会有性能问题。如果有not null的唯一索引,就会用这个索引当做主键,就不会自动创建主键。
主键值最好是连续的,它有这些好处
- 按顺序添加数据页,新数据都在新的数据页,可以一次性写入磁盘,性能更好。
- 可以避免数据页的分裂,减少数据碎片,占用索引空间更小。
也会存在一些问题
在高并发的场景会有写入竞争,主键上界会成为热点,导致间隙锁的竞争。如果用到了自增,还会对自增锁有竞争。
自增锁可以通过修改自增算法来优化,修改innodb_autoinc_lock_mode的值:
0:传统模式,持有自增锁,执行完语句后释放锁。
1:连续模式,持有mutex锁,在明确插入记录数 情况下,拿到对应数量的值就释放锁。
2:交叉模式,持有mutex锁,可以并发过去值。对于binlog等于statement的同步方式,可能造成master和slaver的值不一样。
影响使用索引的情况
虽然在执行计划里显示会使用索引,但实际情况查询速度还是很慢,以下这些因素会影响索引使用。
数据频繁更新
对于二级索引,没有MVCC机制,为了保证索引数据准确性,Mysql会在更新事务提交的时候,把二级索引上的旧记录标记为删除,然后插入一条新数据。在其他进行中的事务读到删除的数据时,就要回表根据MVCC的逻辑查一下数据真实的状态,如果删除了,就不返回。当索引里很多满足的记录都被更新时,那就都要回表查询,这个过程就会影响索引的效率。
为了缓解这个问题,Mysql在二级索引上记录了当前已提交的最大事务ID(MAX_TRX_ID),当查询索引的事务视图中,最消活跃ID大于MAX_TRX_ID,说名事务开启的时候,对这条索引的修改已经提交了,就不要去检查删除的索引数据。
损坏的索引文件
当索引文件因为各种原因会出现损坏的情况,就会导致出现莫名其妙的问题了,返回错误的结果或出现主键冲突等。这些情况都会使查询结果不符合我们的预期,这时候可以通过check table命令检查表索引是否损坏,然后通过repair table命令修复表索引。有的引擎不支持该命令,也可以通过没有操作的alter命令来重建索引,例如alter table user engin=innodb;
查询未使用的索引
通过sys.table_io_waits_summary_by_index_usag找到未使用的索引,及时将无效的索引删除
索引的构建
数据库以排序索引的方式构建btree索引,构建过程分为三步
- 扫描聚簇索引获取需要的列信息,并保存到排序缓冲区中。当缓冲区满时,会将数据排序后写入到临时文件。
- 将多个临时文件的数据合并排序,得到一个总的索引序列。
- 将排序后的数据按顺序插入到btree末尾,这个过程是多线程的。
排序索引对比之前的方式的有点是效率更高,原有的索引构建方式是查出一条数据,就插入btree随机的位置,这会导致索引页频道的分裂和合并。
空间索引使用的是R树,不支持排序索引。