MySQL优化四,高性能优化


一,查询优化器

这个部分的整个过程是由MySQL的存储引擎来做的,优化器就会根据存储引擎来使用原来的开销, 优化后的开销,哪个更好一点?

1.如果是查询语句(select语句),首先会查询缓存是否已有相应结果,有则返回结果,无则进行下一步(如果不是查询语句,同样调到下一步) 2.解析查询,创建一个内部数据结构(解析树),这个解析树主要用来SQL语句的语义与语法解析; 3.优化:优化SQL语句,例如重写查询,决定表的读取顺序,以及选择需要的索引等。这一阶段用户是可以查询的,查询服务器优化器是如何进行优化的,便于用户重构查询和修改相关配置,达到最优化。这一阶段还涉及到存储引擎,优化器会询问存储引擎,比如某个操作的开销信息、是否对特定索引有查询优化等。

二,高性能的索引使用策略

2.1,不要在索引列上做任何的操作

  • 在SQL语句上做表达式
  • 函数
  • 其他操作

2.2,尽量全值匹配

建立了联合索引列后,如果我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配,比方说下边这个查找语句:
 EXPLAIN select * from order_exp where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time='2021-03-22 18:35:14';
我们建立的u_idx_day_statusr索引包含的3个列在这个查询语句中都展现出来了,联合索引中的三个列都可能被用到。 有的同学也许有个疑问,WHERE子句中的几个搜索条件的顺序对查询结果有啥影响么? 放心,MySQL没这么蠢,查询优化器会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件,后使用哪个搜索条件。   所以,当建立了联合索引列后,能在where条件中使用索引的尽量使用。

2.3,最佳左前缀法则

建立了联合索引列,如果搜索条件不够全值匹配怎么办?在我们的搜索语句中也可以不用包含全部联合索引中的列,但要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。 搜索条件中必须出现左边的列才可以使用到这个B+树索引
EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' and order_status=1;     EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' ;       搜索条件中没有出现左边的列不可以使用到这个B+树索引 EXPLAIN SELECT * FROM order_exp WHERE order_status=1;     EXPLAIN Select * from s1 where order_status=1 and expire_time='2021-03-22 18:35:14';  

那为什么搜索条件中必须出现左边的列才可以使用到这个B+树索引呢?比如下边的语句就用不到这个B+树索引么?
因为B+树的数据页和记录先是按照insert_time列的值排序的,在insert_time列的值相同的情况下才使用order_status列进行排序,也就是说insert_time列的值不同的记录中order_status的值可能是无序的。而现在你跳过insert_time列直接根据order_status的值去查找,怎么可能呢?expire_time也是一样的道理,那如果我就想在只使用expire_time的值去通过B+树索引进行查找咋办呢?这好办,你再对expire_time列建一个B+树索引就行了。
但是需要特别注意的一点是,如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列。比方说联合索引u_idx_day_status中列的定义顺序是 `insert_time`, `order_status`, `expire_time`,如果我们的搜索条件中只有insert_time和expire_time,而没有中间的order_status,   EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' and expire_time='2021-03-22 18:35:14';
  请注意key_len,只有5,说明只有insert_time用到了,其他的没有用到。

2.4,范围条件放最后

这一点,也是针对联合索引来说的,前面我们反复强调过,所有记录都是按照索引列的值从小到大的顺序排好序的,而联合索引则是按创建索引时的顺序进行分组排序。
比如: EXPLAIN select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00';     由于B+树中的数据页和记录是先按insert_time列排序的,所以我们上边的查询过程其实是这样的:
找到insert_time值为'2021-03-22 18:23:42' 的记录。 找到insert_timee值为'2021-03-22 18:35:00'的记录。
由于所有记录都是由链表连起来的,所以他们之间的记录都可以很容易的取出来,找到这些记录的主键值,再到聚簇索引中回表查找完整的记录。
但是如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引:   select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00' and order_status > -1;   上边这个查询可以分成两个部分:
通过条件insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00' 来对insert_time进行范围,查找的结果可能有多条insert_time值不同的记录,
对这些insert_time值不同的记录继续通过order_status>-1条件继续过滤。
这样子对于联合索引u_idx_day_status来说,只能用到insert_time列的部分,而用不到order_status列的部分(这里的key_len和之前的SQL的是一样长),因为只有insert_time值相同的情况下才能用order_status列的值进行排序,而这个查询中通过insert_time进行范围查找的记录中可能并不是按照order_status列进行排序的,所以在搜索条件中继续以order_status列进行查找时是用不到这个B+树索引的。
所以对于一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找:   EXPLAIN select * from order_exp_cut where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time>'2021-03-22 18:23:57' and expire_time<'2021-03-22 18:35:00' ;
而中间有范围查询会导致后面的列全部失效,无法充分利用这个联合索引:   EXPLAIN select * from order_exp_cut where insert_time='2021-03-22 18:23:42' and order_status>-1 and expire_time='2021-03-22 18:35:14';

2.5,覆盖索引尽量使用

  注意:尽量不要使用 * ,例如:select * from t_table where ...,最佳写法,把 * 换成索引中的列和主键

覆盖索引是非常有用的工具,能够极大地提高性能,三星索引里最重要的那颗星就是宽索引星。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处:
索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中。
因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),不是必要的情况下减少select*,除非是需要将表中的全部列检索后,进行缓存。   EXPLAIN  select * from order_exp_cut where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time='2021-03-22 18:35:04' ;
使用具体名称取代   EXPLAIN  select expire_time,id from order_exp_cut where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time='2021-03-22 18:35:04' ;
解释一下Extra中的Using index
当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以**使用索引覆盖的情况**下,在Extra列将会提示该额外信息。以上的查询中只需要用到u_idx_day_status而不需要回表操作:

2.6,慎用不等于

注意:使用 “!=”或者“<>”无法使用索引导致全表扫描的

EXPLAIN  SELECT * FROM order_exp WHERE order_no <> 'DD00_6S';

2.7,Null/Not有影响

表order_exp的order_no为索引列,同时不允许为null


explain SELECT * FROM order_exp WHERE order_no is null; explain SELECT * FROM order_exp WHERE order_no is not null;
可以看见,order_no is null的情况下,MySQL直接表示Impossible WHERE(查询语句的WHERE子句永远为FALSE时将会提示该额外信息),对于 is not null直接走的全表扫描。

表order_exp_cut的order_no为索引列,同时允许为null

explain SELECT * FROM order_exp_cut WHERE order_no is null; explain SELECT * FROM order_exp_cut WHERE order_no is not null;
is null会走ref类型的索引访问,is not null;依然是全表扫描。所以总结起来: is not null容易导致索引失效,is null则会区分被检索的列是否为null,如果是null则会走ref类型的索引访问,如果不为null,也是全表扫描。

但是当联合索引上使用时覆盖索引时,情况会有一些不同(order_exp_cut表的order_no可为空):

explain SELECT order_status,expire_time FROM order_exp WHERE insert_time is null; explain SELECT order_status,expire_time FROM order_exp WHERE insert_time is not null;   explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time is null; explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time is not null; 根据system>const>eq_ref>ref>range>index>ALL 的原则,看起来在联合索引中,is not null的表现会更好(如果列可为null的话),但是key_len的长度增加了1。 所以总的来说,在设计表时列尽可能的不要声明为null。  

2.8,Like查询要当心


like以通配符开头('%abc...'),mysql索引失效会变成全表扫描的操作 explain SELECT * FROM order_exp WHERE order_no like '%_6S';
此时如果使用覆盖索引可以改善这个问题
explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time like '%18:35:09';  

 2.9,字符类型加引号

字符串不加单引号索引失效
explain SELECT * FROM order_exp WHERE order_no = 6; explain SELECT * FROM order_exp WHERE order_no = '6';   MySQL的查询优化器,会自动的进行类型转换,比如上个语句里会尝试将order_no转换为数字后和6进行比较,自然造成索引失效。  

2.10,使用or关键字时要注意

explain SELECT * FROM order_exp WHERE order_no = 'DD00_6S' OR order_no = 'DD00_9S'; explain SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09'  OR order_note = 'abc';
  表现是不一样的,第一个SQL的or是相同列,相当于产生两个扫描区间,可以使用上索引。
第二个SQL中or是不同列,并且order_note不是索引。所以只能全表扫描
当然如果两个条件都是索引列,情况会有变化:   explain  SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09'  OR order_no = 'DD00_6S'; 这也给了我们提示,如果我们将 SQL改成union all explain SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09'                     union all SELECT * FROM order_exp WHERE order_note = 'abc';
当然使用覆盖扫描也可以改善这个问题:   explain SELECT order_status,id FROM order_exp_cut WHERE insert_time='2021-03-22 18:34:55' or expire_time='2021-03-22 18:28:28';  

2.11,使用索引扫描来做排序和分组


  MySQL有两种方式可以生成有序的结果﹔通过排序操作﹔或者按索引顺序扫描施﹔如果EXPLAIN出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。
  扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。 这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型的工作负载时。
MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当0RDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。

2.11.1.排序要当心

**ASC、DESC别混用**
对于使用联合索引进行排序的场景,我们要求各个排序列的排序顺序是一致的,也就是要么各个列都是ASC规则排序,要么都是DESC规则排序。 排序列包含非同一个索引的列
用来排序的多个列不是一个索引里的,这种情况也不能使用索引进行排序   explain SELECT * FROM order_exp order by order_no,insert_time;

2.12,尽可能按主键顺序插入行


最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用。例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕,它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。
注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长﹔另一方面毫无疑问是由于页分裂和碎片导致的。
因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。
如果新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置-—通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是总结的一些缺点:
  1. 写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO。
  2. 因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
  3. 所以使用InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能地使用单调增加的聚簇键的值来插入新行。
   

2.13,优化limit分页


在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。
一个非常常见又令人头疼的问题就是,在偏移量非常大的时候,例如可能是   select * from order_exp limit 10000,10;   这样的查询,这时MySQL需要查询10010条记录然后只返回最后10条,前面10 000条记录都将被抛弃,这样的代价非常高。 优化此类分页查询的一个最简单的办法是 会先查询翻页中需要的N条数据的主键值,然后根据主键值回表查询所需要的N条数据,在此过程中查询N条数据的主键id在索引中完成,所以效率会高一些。   EXPLAIN SELECT * FROM (select id from order_exp limit 10000,10) b,order_exp                     a where a.id = b.id;
从执行计划中可以看出,首先执行子查询中的order_exp表,根据主键做索引全表扫描,然后与a表通过id做主键关联查询,相比传统写法中的全表扫描效率会高一些。 从两种写法上能看出性能有一定的差距,虽然并不明显,但是随着数据量的增大,两者执行的效率便会体现出来。 上面的写法虽然可以达到一定程度的优化,但还是存在性能问题。最佳的方式是在业务上进行配合修改为以下语句:
EXPLAIN select * from order_exp where id > 67 order by id limit 10;   采用这种写法,需要前端通过点击More来获得更多数据,而不是纯粹的翻页,因此,每次查询只需要使用上次查询出的数据中的id来获取接下来的数据即可,但这种写法需要业务配合。  

这个系统变量有三个候选值:

nulls_equal:认为所有NULL值都是相等的。这个值也是innodb_stats_method的默认值。
如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向于不使用索引进行访问。
nulls_unequal:认为所有NULL值都是不相等的。
如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问。
nulls_ignored:直接把NULL值忽略掉。
而且有迹象表明,在MySQL5.7.22以后的版本,对这个innodb_stats_method的修改不起作用,MySQL把这个值在代码里写死为nulls_equal。也就是说MySQL在进行索引列的数据统计行为又把null视为第二种情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL中对Null值的处理也很分裂。所以总的来说,对于列的声明尽可能的不要允许为null。