索引介绍
索引的概念
索引的优缺点
优点:
- 查找:提高数据检索效率,降低IO成本。
- 排序:通过索引对数据进行排序,降低排序成本,降低cpu消耗。
缺点:
- 实际上索引也是一张表,该表保存了主键与索引字段,并指向索引记录,索引列也需要占空间。(占有磁盘空间)
- 更新表时(insert、update、delete)不仅要保存数据还要更新保存索引文件新添加索引列。降低了速率。
索引的分类
- 主键自动建主键索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其他表关联的字段,外键关系建立索引
- 在高并发下倾向建立联合索引
- 查询中的排序字段,排序字段若通过索引去访问将大大提高排序速度
-
哪些情况不适合建索引
频繁更新的字段
- where条件用不到的字段不创建索引
- 表记录太少
- 经常增删改的表
数据重复太多字段,为它建立索引意义不大(假如一个表有10万,有一个字段只有T和F两种值,每个值的分布概率大约只有50%,那么对这个字段的建索引一般不会提高查询效率,索引的选择性是指索引列的不同值数据与表中索引记录的比,,如果一个表中有2000条记录,表中索引列的不同值记录有1980个,这个索引的选择性为1980/2000=0.99,如果索引项越接近1,这个索引效率越高)
慢查询(未命中索引)
慢查询的定义
mysql的慢查询日志是mysql提供的一种日志记录,用来记录在mysql中响应时间超过阈值的语句,具体指运行时间超过long_query_time值得SQL,则会被记录在慢查询日志中。
慢查询相关参数
show_query_log:是否开启慢查询日志,1表示开启,0表示关闭。
- log-slow-queries:旧版(5.6以下版本)mysql数据库慢查询日志存储路径。可以不设置该参数,系统会默认给一个缺省得文件host_name_slow.log
- slow-query-log-file:新版(5.6以上版本)mysql数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省得文件host_name_slow.log
- long_query_time:慢查询阈值,当查询时间高于设定得阈值时,记录到日志
- log_queries_not_using_indexes:未使用索引的查询也被记录到慢查询日志中(可选项)
默认情况下slow_query_log的值为OFF,表示慢查询日志是禁用的,可以通过设置slow_query_log的值来开启,如:
使用set global slow_query_log=1 开启了慢查询日志只对当前数据库生效,如果MySQL重启后则会失效。如果要永久生效,必须修改配置文件 my.cnf
ong_query_time的默认值为10 秒,支持二次修改。线上我们一般会设置成1秒,如果业务对延迟敏感的话,我们根据需要设置一个更低的值。
explain字段分析
explain是排查慢sql的一种常见的手段
mysql> EXPLAIN SELECT 1;

id:表示select子句或者操作的顺序
- id相同:执行顺序自上而下
- id不同:id值越大优先级越高,越先被执行
- id相同不同:id越大越先执行,相同的自上而下执行
select_type:主要是区分普通查询、联合查询、子查询等。
- SIMPLE:简单的select查询,不包含子查询与union
- PRIMARY:查询中包含复杂的子部分,最外层会被标记为primary
- SUBQUERY:在select或者where列表中包含了子查询
- DERIVED:在from列表中包含的子查询衍生表
- UNION:若第二个select出现在union之后,则被标记为union
- UNION RESESULT:从union表获取结果的select
table:这一行数据是哪个表的数据
type:查询中使用了何种类型
结果值从最好到最坏:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>all
- 一般来说,得保证查询至少达到range级别,最好能到达ref
- ref:非唯一性索引扫描,返回匹配某个单独值的所有行
- range:只检索给定范围的行,使用一个索引来选择行,一般就是在where语句中出现了between、<、>、in等的查询
- index:index比all快,因为index是从索引中读取,all是从硬盘中读取
- all:遍历全表才能找到
Extra:包含不适合在其他列中显示,但十分重要的的额外信息
- Using filesort 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,mysql中无法利用索引完成的排序成为文件排序
- Using temporary 使了用临时表保存中间结果,mysql在对查询结果排序时使用了临时表,常见于排序order by 和分组查询group by
- Using index 表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率高
- Using where 表明使用了where进行过滤
- Using join buffer 使用了连接缓存
- impossible where 如果where子句的值总是false,不能用来获取任何元组
select table optimized away 在没有group by子句的情况下,基于索引优化min/max操作或者对于myisam存储引擎优化count(*)操作,不必等到执行阶段再进行计算
索引失效
应该尽量全值匹配
- 复合最佳左前缀法则(第一个索引不能掉,中间不能断开)
- 不在索引列上做任何操作(计算、函数、类型转换)会导致索引失效而转向全表扫描
- 存储存引擎不能使用索引中范围条件右边的列
- 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select*
- mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描
- is null,is not null也会无法使用索引
- like以统配符开头
- 字符串不加单引号
-
order by优化
避免filesort,尽量在索引上进行排序,遵照最佳左前缀原则
filesort有两种排序:
- 双路排序:两次磁盘扫描
- 单路排序:一次性读取保存在内存中,没拉完的数据再次拉
- 单路排序总体好于双路排序
- 优化策略:1、增大sort_buffer_size参数的设置,2、增大max_length_for_sort_data参数的设置,尽可能一次拿到内存
参考文献:https://mp.weixin.qq.com/s/sL64uQP0iHKxkMFx1QGLkg
慢查询(命中索引)
首先看以下几种场景的SQL语句执行时,命中的索引情况
1.执行 explain select from user; ,发现key这列为null,说明了没有命中索引,走了全表扫描。
2.执行 explain select from user where id= 10; ,发现key这列为PRIMARY,说明使用了主键索引。
3.执行 explain select user_name from user;,发现key这列为idx_user_name,说明使用了二级普通索引。
但是,实验发现,虽然走了二级索引,但是rows扫描行为79741,说明走了全表扫描,性能很差。
如果请求并发量很高,很容易引发数据库连接无法及时释放,导致客户端无法获取数据库连接而报错。
我们知道所有的数据都是存储在 B+ 索引树上,当执行 explain select * from user where id>0; 时,发现使用了主键索引。
mysql优化器根据主键索引找到第一个id>0的值,虽然走了索引但其实还是全表扫描。
没命中索引会走全表扫描,命中了索引依旧有可能走全表扫描
判断SQL性能时,不一定看是否命中索引;还要看扫描行数;当表很大时,不仅关注是否有索引,还要关注索引的过滤性是否足够好。
回表优化
为user表增加一个user_name和age的联合索引
ALTER TABLE `user` ADD INDEX idx_user_name_age ( `user_name`,`age` );

执行explain select * from user where user_name like ‘Cherry1%’ and age =18;
执行流程:
- 第一步:首先在idx_user_name_age索引树,查找第一个以’Cherry1开头的记录对应的主键id
- 第二步:根据主键id从主键索引树中找到整行记录,并根据age做判断过滤,等于18则留下,否则丢弃,则这个过程也成为回表
- 第三步:然后,在idx_user_name_age联合索引树上向右遍历,找到下一个主键id
- 第四步:在执行第二步
- 第五步:后面重复执行第三步,第四步,直到user_name不是以Cherry1开头,则结束。
- 第六步:返回所有查询结果
分析:
由于按user_name 的前缀匹配,idx_user_name_age二级索引中的 age 部分并没有发挥作用。导致了大量回表查询,性能较差。
优化后,执行流程:
- 第一步:首先在idx_user_name_age索引树,查找一个以Cherry1开头的索引记录
- 第二步:然后,判断这个索引记录中的age是否等于18.如果是回表取出整行数据,作为后面的结果返回;如果不是则丢弃。
- 第三步:在idx_user_name_age联合索引树上向右遍历,重复第二步,直到user_name不是以Cherry1开头,则结束。
- 第四步:返回所有查询结果
和上面的过程差别,在于判断age是否等于18放在遍历联合索引过程中进行,不需要回表判断,大大降低了回表的次数,提升性能。
这个优化遵循最左前缀原则,索引的过滤性仍然有提升空间。
虚拟列
mysql5.7之后引入了虚拟列。 在MySQL 5.7中,支持两种Generated Column,即Virtual Generated Column和Stored Generated Column,前者只将Generated Column保存在数据字典中(表的元数据),并不会将这一列数据持久化到磁盘上;后者会将Generated Column持久化到磁盘上,而不是每次读取的时候计算所得。很明显,后者存放了可以通过已有数据计算而得的数据,需要更多的磁盘空间,与Virtual Column相比并没有优势,因此,MySQL 5.7中,不指定Generated Column的类型,默认是Virtual Column。
修改表结构
ALTER TABLE `user` add user_name_first varchar(12) generated always as (left(user_name,6)) , add index(user_name_first,age);

执行 explain select * from user where user_name_first like ‘Cherry1%’ and age =18;
比较发现,扫描行数row变小了,证明优化有效。
slow_query_log 收集到的慢 SQL ,结合 explain 分析是否命中索引,结合扫描行数,有针对性的优化慢 SQL。
但是要注意一点,慢 SQL 日志中也可能有正常的 SQL,可能只是当时CPU等系统资源过载,影响到正常 SQL 的执行速度。慢查询和索引没有必然联系,一个SQL语句的执行效率最终要看的是扫描行数。另外可以使用虚拟列和联合索引来提升复杂查询的执行效率。
