开发同学或多或少会遇到系统响应慢的问题,除了业务系统本身的问题外,常常会遇到SQL查询慢
的问题,这篇文章结合实际案例分析MySQL InnoDB存储引擎的索引优化,这篇文章不会介绍B+
树的知识点,如果需要了解聚集索引和辅助索引特点的同学可以参考这篇文章,这篇文章主要会介绍三星索引
和ICP优化
.
如何分析SQL性能
首先是查看MySQL的状态,系统是否正常,常用的几个命令如下:
#显示状态信息(扩展show status like ‘XXX’)
Mysql> show status;
#显示系统变量(扩展show variables like ‘XXX’)
Mysql> show variables\G;
#显示InnoDB存储引擎的状态
Mysql> show engine innodb status\G;
#查看当前SQL执行,包括执行状态、是否锁表等
Mysql> show processlist ;
第二步是找出系统有哪些慢查询SQL,这个要通过slowLog来查询,首先开启慢查询日志,然后在对应日志路径找到mysql-slow.log,相关命令如下所示:
# 检查是否开启慢查询日志
show variables like '%slow%';
# 如果没有开启,也可以在运行时动态开启这个参数
set global slow_query_log=ON;
# 设置慢查询记录查询耗时多长的SQL,这里设置成100毫秒
set long_query_time = 0.1;
# 这里休眠500毫秒试一下慢查询日志是否会记录
select sleep(0.5)
找到了慢SQL后比较常见的做法就是用explain命令分析SQL执行计划,查看SQL语句是否命中了索引,explain的用法可以参考MySQL 性能优化神器 Explain 使用分析,在优化过程中我们可能需要看到优化前后的查询时间对比,这时候可以打开profiling开关,查看某条SQL语句的执行耗时情况,分析是哪个步骤耗时较长,相关设置如下:
# 查看是否开启profiling
select @@profiling;
# 开profiling,注意测试完关闭该特性,否则耗费资源
set profiling=1;
# 查看所有记录profile的SQL
show profiles;
# 查看指定ID的SQL的详情
show profile for query 1;
# 测试完,关闭该特性
set profiling=0;
一个profiling例子:
三星索引优化策略
三星索引的策略是根据查询语句来建立联合索引,比如有这样一条SQL,SELECT GroupId,AddTime,Status from Order WHERE GroupId = 10010 ORDER BY AddTime
,如何用三星索引来优化这条查询呢?
-
一星索引的核心就是利用索引来尽可能的过滤不必要的数据,减少数据处理的规模,对于RDBMS来说是极为关键的,比如说Order表有100万行数据,GroupId的过滤度(cardinality)是90%,Status的过滤度是0.1%,,如果没有建立索引,那要扫描100万行数据去找到GroupId等于10010的那一条数据,因此这里需要把GroupId作为联合索引的第一列,你是不是加了GroupId的索引就解决问题了呢,答案是否定的,虽然GroupId索引过滤了大部分数据,但由于select 后面有order by语句,而且这条SQL查询的不止GroupId这一个字段,不可避免要二次IO,而且还要在内存里进行一次filesort,explain的执行结果如下所示:
从上图可以看到这条SQL使用了GroupId这个索引,但是Extra里面并不是直接Using Index
,而是Using Index Condition
,这意味着通过索引无法完成这条查询,存储引擎还是得读取对应的记录来完成查询,不过这里使用了ICP
技术把GroupId
这个索引下推到存储引擎层进行过滤,而且在内存中进行了一次filesort
排序. -
这里就要用到二星索引来优化
filesort
了,filesort
一般出现在要排序的字段没有添加索引或者即使添加了索引但索引不是有顺序的情况下,需要在内存中进行一次排序然后再返回给客户端,如果数据量比较大的情况下排序的开销也挺大,二星索引基本的想法就是利用索引的有序性,消除orderby或者group by等需要排序的操作(备注:group by语句默认会对该字段排序),排序是非常消耗CPU资源的,大量的排序操作会把user cpu搞得很高,即使CPU吃得消,如果数据量比较大,需要排序的数据放不下内存的sort buffer,只能悲剧的和外存换进换出,性能下降的就不是一点两点了,这时候利用索引避免排序的优势就明显的体现出来了,这时候就需要建立GroupId和AddTime的联合索引了,当GroupId相等的时候联合索引的第二列AddTime就默认是有顺序的,这样就可以避免filesort,修改索引后的效果如下所示:
-
从上面的图片可以看到filesort已经没有了,但是还是使用ICP而不是Using Index,这是因为我们Select的字段还包含Status这一列,而这一列不在联合索引中,因此存储引擎还是需要读取该行记录来获取Status的值,这时候三星索引就派上用场了, 在索引中额外添加要查询的列Status,这就是所谓的索引覆盖,即在索引的叶子节点就能够读到查询SQL所需要的所有信息,而不需要回原表去查询,如下图所示:
这里的Extra列显示是Using Where,Using Index,意思是数据是从索引里面取,不需要回表,Using where表示需要根据其他列来过滤数据,从key_len列也看出索引的长度为5,表示只用了GroupId这个索引,索引长度的计算可以参考这篇文章 除了order by,group by的字段适合作为二星索引外,范围查询(包括between and)也适合,但是当order by和范围查询同时存在时,就需要根据实际情况作出取舍了,看是filesort的开销比较大还是范围查询的开销大
关于三星索引,并不是说要把select语句里所有的查询字段都加到索引列,索引字段太多会给数据库带来很大的开销,特别是会影响DML语句的性能,而且还要结合cardinality字段来分析,有些字段的取值范围很小,选择度很低,不适合作为索引.
ICP技术介绍
前面的章节多次提到了ICP技术
,那到底什么是ICP
呢,在介绍ICP
之前我们先来看一下SQL
语句的where条件提取规则:所有SQL的where条件,都可以归纳为3大类:Index Key
,Index Filte
r和Table Filter
,下面的介绍以CREATE TABLE Order (GroupId int(11) DEFAULT NULL, AddTime datetime DEFAULT NULL, Status int(11) DEFAULT NULL,KEY IX_GroupId (GroupId), KEY IX_AddTime (AddTime)) ENGINE=InnoDB
这个表结构为例,假设我们分别单独建立了groupId
和AddTime
的索引,假设我们要执行这样一条SQLselect groupid,addtime,status from Order where groupid < 10010 and addTime > '1970' and status = 0
;,那SQL执行计划会是怎样呢?
Index Key
Index Key
只是用来定位索引的起止范围,因此只在索引第一次搜索时使用,一次判断即可;
从起始范围之后读到的每一条索引记录,均需要判断是否在Index Key的范围内,以上面的SQL为例,Index Key就是GroupId,它的范围是GroupId < 10010.
Index Filter
用于过滤索引查询范围中不满足查询条件的记录,因此对于索引范围中的每一条记录,均需要与Index Filter
进行对比,若不满足Index Filter
则直接丢弃,继续读取索引下一条记录,在前面的SQL中,Index Filter
就是AddTime > '1970'
,MySQL会把这个过滤条件下推到存储引擎层,ICP就是Index Condition Push Down,不管是Index Key
还是Index Filter
对应的列必须添加了索引.
Table Filter
Table Filter
是Where
条件最后一道防线,用于过滤通过前面索引的层层考验的记录,此时的记录已经满足了Index Key
的范围,并且符合Index Filter
的条件,存储引擎通过回表读取了完整的记录,判断整条记录是否满足Table Filter
中的查询条件,同样的,若不满足,跳过当前记录,继续读取索引的下一条记录,若满足,则返回记录,此记录满足了where的所有条件,可以返回给前端用户.还是回到前面那条SQL,Where条件中的Status=0就是Table Filter,因为Status列并没有添加索引,所以只能通过回表读取该字段来过滤.
我们来总结下ICP
的过程,MySQL
把Index Filter
条件AddTime>'1970'
下推到存储引擎,存储引擎根据AddTime
索引来过滤掉不符合where
条件AddTime>'1970'
的记录,然后把最终数据返回给MySQL Server
,MySQL Server
再根据where
条件status=0
来做最后的过滤,最后把数据返回给用户,这个过程减少了二次回表的次数,有效减少了磁盘IO.最后我们来看下explain语句的结果:
其他优化建议
- 整型数据比起字符,处理开销更小,在MySQL中,应该用内置的日期和时间数据类型,而不是用字符串来存储时间;
- 尽量指定列为NOT NULL,除非你想存储NULL,在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂,你应该用0、一个特殊的值或者一个空串代替空值;
- 当结果集只需要一行数据时使用LIMIT 1
- 避免SELECT *,始终指定你需要的列
- 使用连接(JOIN)来代替子查询(Sub-Queries),使用Join来代理大的分页语句,比如select ... Limit 1000000,10 这条语句MySQL要根据查询条件找到符合的100万条记录,然后删掉再读取后10条记录,应该根据Where条件把主键查出来,然后根据主键去访问数据,比如select * from Order o inner join (select Id from Order where ...) t where o.id=t.id limit 1000000,10;
- where子句的查询条件里有!=,MySQL将无法使用索引
- 使用LIKE进行搜索匹配的时候,这样索引是有效的:select * from Order where name like ‘xxx%’,而like ‘%xxx%’` 时索引无效