在上文中,我们介绍了MySQL中的两种主要的索引–B-Tree索引和Hash索引。虽然使用索引会带来很大程度上的性能优化。但是,索引的不当使用同时也会引起性能的急剧下降。今天我们就来聊聊索引的优化问题。
首先讨论两个问题:
谈到为什么要使用索引,大家第一时间想到的就是提升查询效率吧。那除了这个用途之外索引还有其他的用途吗?下面我们就来聊一聊:
1)、使用索引大大减少了存储引擎需要扫描的数据量。例如:在InnoDB存储引擎中,默认索引的一页为16k,可以存放更多的数据。
2)、索引可以帮助我们进行排序以避免使用临时表。例如:B-Tree索引用于排序,减少临时表的IO消耗,提高MySQL的处理能力。
3)、因为数据库中数据的物理地址一般是随机存放,而索引是顺序存储的,所以索引可以把随机I/O变为顺序I/O。
答案肯定不是。因为索引同时也会带来性能损耗,主要有以下两个方面:
1)、索引会增加写操作的成本
数据的更新必须维护索引和统计信息。索引越多,修改数据的时间越长。所以引入了插入缓存来减少这种写操作的成本。
这里需要说明的一点是,引入插入缓存并不能增加数据的导入速率。相反提高导入速率的最好方法就是删除索引索引。但应该保留自增的主键ID,否则,导入速率可能更低。
2)、太多的索引会增加查询优化器的选择时间
之前也说过,MySQL查询优化器会为查询选择合适的索引。若是同一个语句有很多索引可以使用,也会增加查询优化器对索引的选择时间。
在解决了上述两个问题之后,我们接下来就具体谈一谈索引的一些优化策略。
如果查询中的列不是独立的,则MySQL是不会使用索引的。比如下面这个示例:
select ID from product where to_days(out_date) - to_days(cur_date) <= 30;
在这条语句中若out_data是索引列,to_days()是函数,则这个查询无法使用out_date列的索引。所以,我们因该养成简化where条件的习惯,始终将索引列单独放在比较符号的一侧。对上条查询语句的优化如下:
select out_date <= data_add(cur_date , interval 30 day);
当需要索引很长的列时,会让索引变得大而且慢。一个策略是上文提到的模拟hash索引。但有时这样做还不够,还可以使用前缀索引减少索引列的长度,提高索引效率。但前缀索引会降低索引的选择性。索引的选择性是指不重复的索引值和表的记录数的比值。索引键值的唯一性越高,选择性越高。唯一索引或主键索引的选择性最高。而选择性越高的索引可以让MySQL在查找时,过滤掉更过的行。
建立前缀索引时,既要保证索引的长度尽可能小,也不能使索引的选择性太低。
前缀索引是一种使索引更小、更快的有效办法,但另一方面也优缺点:MySQL无法使用前缀索引做 order by 和 group by,也无法使用前缀索引做覆盖扫描。
很多人在创建索引时,想到在where子句中的所有列都应该建立索引,所以就会为表中的每个列创建单独的索引,认为这样会提高查询性能。但是,过多的索引同样会造成MySQL的性能下降。
而且MySQL 5.5之前不能使用多个索引进行查询,5.5之后虽然引入了索引合并的概念,可以使用多个独立索引进行合并过滤,但同时也需要耗费大量的CUP和内存资源在算法的缓存排序和合并操作上。更重要的是,MySQL优化器不会把这些计算到“查询成本”,只关心随机页面读取。这会使查询成本被“低估”,导致该执行计划还不如直接走全表扫描。此外,还可能影响查询的并发性。
在需要查询多列时,还可以使用联合索引。而且Btree索引按照索引列的顺序进行存放,而索引列的先后顺序也对应着查询是否能使用到这些索引。那么如何选择索引列的顺序呢?
1、经常会被使用到的列优先。
2、选择性高的列优先。
3、宽度小的列优先。
4、选择性很差的列不应该放在最左边。
前面我们谈到了根据查询的where条件来创建合适的索引。但是设计索引时,应该考虑到整个查询。
MySQL中也可以使用索引来直接获取列的数据,这样就不用读取数据行。如果一个索引包含所有需要查询的字段的值,我们就称为“覆盖索引”。
1、可以优化缓存,减少磁盘IO操作
索引条目通常远远小于数据行的大小,所以如果只需要读取索引,那么MySQL就会极大地减少数据的访问量。这对缓存的负载极为重要,因为这种情况下响应时间大量花费在数据拷贝上。
2、可以减少随机IO,变随机IO操作为顺序IO操作
因为索引是对列值顺序存储的(至少在单页里面是这样),所以对于I/O密集型的范围查询会比随机从磁盘一行数据的I/O要小得多。
3、可以避免对InnoDB主键索引的二次查询
InnoDB的二级索引在叶子结点中保存了行的主键值,所以二级索引如果能够覆盖查询,则可以避免对主键索引的二次查询。
4、可以避免MyISAM表进行系统调用
MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占据了数据访问中的最大开销的场景。
但是,不是所有类型的索引都能成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引、全文索引都不存在存储索引列的值,所以MySQL只能使用B-tree索引所覆盖索引。
1、存储引擎不支持覆盖索引
2、查询中使用了太多的列。比如:select * … 没有任何索引能够覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。
3、使用了双%号的like查询。这是因为底层存储引擎API的限制,这样的查询MySQL只能提取数据行,然后再内存中进行过滤。
MySQL有两种方式可以生成有序的结果:
1)、通过排序操作
2)、按照索引顺序扫描
如果EXPLAIN出来的TYPE的列值为“index”,则说明了MySQL使用了索引扫描来做排序。扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但是这并不太容易,下面是一些使用索引扫描来优化排序的一些条件
1、索引列顺序和order by 子句的顺序完全一致
2、索引列中的所有列的方向(升序、降序)和order by子句完全一致
3、order by中的字段全部在关联表中的第一张表中
索引可以让查询锁定更少的行。如果你的查询从不访问哪些不需要的行,就会锁定更少的行,从两个方面来看这都对性能有好处:
1)、虽然InnoDB的行锁效率很高,内存也很少使用,但锁定行的时候仍会带来额外开销;
2)、锁定超过需要的行会增加锁争用并减少并发性。
InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行的时候才有效。若索引无法过滤掉无效的行,那么InnoDB检索到数据并将返回给服务器层以后,MySQL服务器才能应用where子句。这时,已经无法避免锁定行了:InnoDB已经锁定了这些行,到适当的时候才释放。在早期的时候InnoDB只有在事务提交之后才能释放所。而在MySQL5.1之后,InnoDB可以在服务器端过滤行后就释放锁。
MySQL允许在相同的列上创建多个索引。但带来的问题就是:MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个进行考虑,这同样会影响性能。
重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。如下:
(1)、primary key(id) , unique(id) , index(id)
事实上,MySQL的唯一限制和主键限制就是通过索引实现的,因此,上述的写法实际上在相同的列上创建了三个重复的索引。通常并没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求(若索引类型不同 , 并不算是重复索引。例如可以创建 KEY(col)和FULLTEXT KEY(col))。
(2)、Index(a) , Index(a , b)
(3)、primary key(id) , index(a , id)
(2) 中单独查询a列也会使用联合索引。所以Index(a)就是一个冗余索引。但若是创建Index(a) , 之后在创建 Index(b,a)这就不是冗余索引了。
(3) 中id是主键,对于InnoDB来说主键已经包含在二级索引中了,所以这也是冗余的
在某大多数情况下,我们都不需要冗余索引,应尽量扩展已有的索引而不是创建新的索引。但是也有时候出于性能的考虑也需要使用冗余索引,扩展已有的索引会将其变得很大,从而影响其他使用该索引的查询的性能。
比如,若在整数列上有一个索引,现在需要额外增加一个很长的varchar列来扩展该索引,那么性能可能会急剧下降。特别是当有查询把这个索引当作覆盖索引,或者是MyISAM表并且有很多范围查询(前缀压缩)的时候。这时就需要两个索引,尽管这样一来原来的单列索引是冗余的了。
除了重复索引和冗余索引,还可能有一些很少使用或永远不用的索引。我们需要定期去维护这些索引。若是不需要则可以将其删除。
那么如何查找这些很少使用的索引呢,这里有一个sql语句可以帮助统计每个数据库中的每个表的所有索引的使用情况。
SELECT object_schema,object_name,index_name,b.TABLE_ROWS
FROM `performance_schema`.table_io_waits_summary_by_index_usage a
join information_schema.`TABLES` b on
a.object_schema=b.TABLE_SCHEMA and
a.object_name=b.TABLE_NAME
where index_name is not null and count_star=0
order by object_schema,object_name;
Mysql的查询优化器会根据索引的统计信息决定使用那一个索引来优化我们的查询,若
索引的统计信息不准确的话,查询优化器就会做出错误的判断。可以通过下面的sql语句重新生成索引统计信息:
analyze table table_name
需要说明的是:不同的存储引擎生成和保存统计信息的方式也不同。运行该命令的成本也不同。例如:myisam会将索引的统计信息存储在磁盘中,进行是会进行全索引的扫描,需要对表进行锁定。效率较低;而Innodb 会通过随机的索引访问的方式进行评估,并将其存储在内存中,效率高,但并不十分准确,只是估算值。
B-Tree索引在更新时会产生大量碎片,会降低查询的效率。碎片化的索引可能会以很差的、无序的方式存储在磁盘上,除此之外,表也会产生碎片。所以为了提高性能,我们应该定期对索引和表碎片进行维护。可以使用下面语句:
optimize table table_name;
注意:执行该语句时会导致锁表,所以需要慎重使用。
最后,以一个小问题结束本文:
索引并不总是最好的工具。总的来说,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作(比如插入操作后索引的维护)时,索引才是高效的。
1)、对于非常小的表: 大部分情况下简单的全表扫描更高效。
2)、中到大型表: 索引非常高效。
3)、特大型表: 建立和使用索引的代价随之增长,可以使用分区技术代替。
4)、表数量特别多: 创建一个元数据信息表,用来查询需要使用到的某些特性。