mysql优化五:索引优化实战(下)

文章目录

  • 索引优化实战(下)
    • join优化
      • mysql 表关联的两种算法
      • 优化方案
    • in和exsits优化
    • count(*)查询优化
    • 索引设计原则
    • sql优化总结

索引优化实战(下)

所用sql

CREATE TABLE `t1` (
	`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
	`a` INT ( 11 ) DEFAULT NULL,
	`b` INT ( 11 ) DEFAULT NULL,
	PRIMARY KEY ( `id` ),
KEY `idx_a` ( `a` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8;

create table t2 like t1;

#‐‐ 往t1表插入1万行记录
 drop procedure if exists insert_t1; 
 delimiter ;; 
 create procedure insert_t1()
 begin 
 declare i int; 
 set i=1; 
 while(i<=10000)do
 insert into t1(a,b) values(i,i);
 set i=i+1;
 end while;
 end;; 
 delimiter ;
call insert_t1();

#‐‐ 往t2表插入100行记录
 drop procedure if exists insert_t2; 
 
 delimiter ;; 
 create procedure insert_t2()
 begin 
 declare i int; 
 set i=1; 
 while(i<=100)do
 insert into t2(a,b) values(i,i);
 set i=i+1;
 end while;
 end;; 
 delimiter ;
call insert_t2();

join优化

mysql 表关联的两种算法

嵌套循环连接 Nested-Loop Join(NLJ) 算法
每一次扫描一行,循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动 表)里取出满足条件的行,然后取出两张表的结果合集。看下面的例子

Nested-Loop Join 算法
mysql优化五:索引优化实战(下)_第1张图片
在关联查询中先执行的表是驱动表,后执行的表是被驱动表。所以t2是驱动表,t1是被驱动表。如果extra中没有出现Using join buffer 那么就是使用了NLJ算法。
上面sql的大致执行流程:

  1. 从t2表中读取一行数据(如果t2表有where条件,就会先过滤)
  2. 从读取到的行数据中找到关联字段,例如案例中的t2.a字段,到t1表中获取数据
  3. 将t1表中获取到的数据和t2表中的数据合并,作为结果反回给客户端
  4. 重复上面三步

整个过程中,从t2获取所有的数据总共要扫描100行,每次扫描到一行数据都要找到关联字段再去t1中找数据,由于关联字段有索引,因此从t1中获取数据不需要一行一行的扫描,可以算作只扫描一次。那么t1表也扫描100行。总共扫描了200行。这是关联字段有索引的情况下。
如果关联字段没有索引,那会是怎么个情况。首先依然是从t2表中扫描100行数据,然后找到关联字段,在去匹配t1表。由于没有索引,每次匹配的时候都要全盘扫描。t1表有1万行数据,就是每次需要扫描1万行,要扫描100次,一共需要扫描100*10000 100万行。也就是说在没有索引的情况这种扫描算法效率极差。这时候就要有另一种算法Block Nested-Loop Join

基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法
把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。看下面的案例
mysql优化五:索引优化实战(下)_第2张图片
从上面explain分析结果看出,extra出现Using join buffer说明使用了BNL算法。
上面sql的大致流程如下:

  1. 把 t2 的所有数据放入到 join_buffer 中
  2. 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比
  3. 返回满足 join 条件的数据

整个过程要从t2表中获取所有数据,需要扫描100次,然后放入join_buffer中,从t1表一行一行的扫描数据和join_buffer中的数据尽心比较,需要扫描1万次,由于join_buffer中的数据是无序,每一次匹配数据都要匹配100次,总共要匹配10000*100 100万次。总的来说,这个案例中需要扫描表10100次,然后100万次数据比较。
再来回顾一下,在Nested-Loop Join算法中也说了,如果没有索引的情况,如果按照NLJ算法,要扫描100万次磁盘。如果按照BNL算法,需要扫描10100次磁盘和100万次的join_buffer比较。虽然这两种算法都出现了100万,但是NLJ算法是扫描磁盘100万次,BNL是内存比较100万次,孰轻孰重这个就不用说了吧。但是就算使用了BNL算法仍然要扫描磁盘10100次。也就是说如果数据量大话,查询效率依然不高。

总结一下:

  1. 在关联查询中先执行的表是驱动表,后执行的表是被驱动表
  2. 当使用left join时,左表是驱动表,右表是被驱动表,当使用right join时,右表时驱动表,左表是被驱动表, 当使用inner join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表。
  3. 一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer 则表示使用的 join 算 法是 NLJ
  4. mysql中 join查询如果关联字段有索引会使用NLJ算法,没有索引会使用BNL算法
  5. join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k
  6. 如果使用BNL算法,驱动表数据量很多,join_buffer 放不下所有数据怎么办,策略很简单, 就是分段放。 比如 t2 表有1000行记录, join_buffer 一次只能放800行数据,那么执行过程就是先往 join_buffer 里放800行记录,然 后从 t1 表里取数据跟 join_buffer 中数据对比得到部分结果,然后清空 join_buffer ,再放入 t2 表剩余200行记录,再 次从 t1 表里取数据跟 join_buffer 中数据对比。所以就多扫了一次 t1 表。

优化方案

  1. 关联字段加索引,让mysql做join操作时尽量选择NLJ算法
  2. 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去 mysql优化器自己判断的时间
    straight_join解释:straight_join功能同join类似,但能让左边的表来驱动右边的表,能改表优化器对于联表查询的执 行顺序。 比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql选着 t2 表作为驱动表。 straight_join只适用于inner join,并不适用于left join,right join。(因为left join,right join已经代表指 定了表的执行顺序) 尽可能让优化器去判断,因为大部分情况下mysql优化器是比人要聪明的。使用straight_join一定要慎重,因 为部分情况下人为指定的执行顺序并不一定会比优化引擎要靠谱。
  3. 对于小表定义的明确。 在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据 量,数据量小的那个表,就是“小表”,应该作为驱动表。

join优化其实没有太好的优化方案,说白了就是explain分析之后对每一条分析结果进行优化。很多大厂一般也不建议使用join来查询。

in和exsits优化

原则:小表驱动大表,即小的数据集驱动大的数据集
in:当B表的数据集小于A表的数据集时,in优于exists select * from A where id in (select id from B)

#等价于: for(select id from B){
  select * from A where A.id = B.id 
 }

exists:当A表的数据集小于B表的数据集时,exists优于in 将主查询A的数据,放到子查询B中做条件验证,根据验证结果(true或false)来决定主查询的数据是否保留

select * from A where exists (select 1 from B where B.id = A.id) 
 #等价于: for(select * from A){
  select * from B where B.id = A.id 
  }
  1. EXISTS (subquery)只返回TRUE或FALSE,因此子查询中的SELECT * 也可以用SELECT 1替换,官方说法是实际执行时会忽略SELECT清单,因此没有区别
  2. EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比
  3. EXISTS子查询往往也可以用JOIN来代替,何种最优需要具体问题具体分析

count(*)查询优化

employees表是之前优化三四中用到的表

#临时关闭mysql查询缓存,为了查看sql多次执行的真实时间
 set global query_cache_size=0; 
 set global query_cache_type=0;
 
 EXPLAIN select count(1) from employees;
 EXPLAIN select count(id) from employees; 
 EXPLAIN select count(name) from employees; 
 EXPLAIN select count(*) from employees;

四个sql的执行计划一样,说明这四个sql执行效率应该差不多
字段有索引:count(*)≈count(1)>count(字段)>count(主键 id),字段有索引,count(字段)统计走二级索引,二级索引存储数据比主键索引少,所以count(字段)>count(主键 id)
字段无索引:count(*)≈count(1)>count(主键 id)>count(字段) ,字段没有索引,count(字段)统计走不了索引,count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)
count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出 字段,所以理论上count(1)比count(字段)会快一点。
count(*)是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用 count(列名)或count(常量)来替代 count(*)。

优化方案

  1. 查询mysql自己维护的总行数 对于myisam存储引擎的表做不带where条件的count查询性能是很高的,因为myisam存储引擎的表的总行数会被 mysql存储在磁盘上,查询不需要计算。对于innodb存储引擎的表mysql不会存储表的总记录行数(因为有MVCC机制,后面会讲),查询count需要实时计算。
  2. show table status 如果只需要知道表总行数的估计值可以用如下sql查询,性能很高
    mysql优化五:索引优化实战(下)_第3张图片
  3. 将总数维护到Redis里 插入或删除表数据行的时候同时维护redis里的表总行数key的计数值(用incr或decr命令),但是这种方式可能不准,很难保证表操作和redis操作的事务一致性
  4. 增加数据库计数表 插入或删除表数据行的时候同时维护计数表,让他们在同一个事务里操作

索引设计原则

1、代码先行,索引后上 不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗? 这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立 索引。
2、联合索引尽量覆盖条件 比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的 where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原 则。
3、不要在小基数字段上建立索引 索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段, 其值不是男就是女,那么该字段的基数就是2。 如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。 一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查找的优势来。
4、长字符串我们可以采用前缀索引 尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会 比较小,此时你在搜索的时候性能也会比较好一点。 当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立 索引,哪怕多占用一些磁盘空间也是有必要的。 对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个 字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY index(name(20),age,position)。 此时你在where条件里搜索的时候,如果是根据name字段来搜索,那么此时就会先到索引树里根据name 字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来 完整的name字段值进行比对。 但是假如你要是order by name,那么此时你的name因为在索引树里仅仅包含了前20个字符,所以这个排序是没法用上索引的, group by也是同理。所以这里大家要对前缀索引有一个了解。
5、where与order by冲突时优先where 在where和order by出现索引设计冲突时,到底是针对where去设计索引,还是针对order by设计索引?到 底是让where去用上索引,还是让order by用上索引?
一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。 因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可 能会小很多。
6、基于慢sql查询做优化 可以根据监控后台的一些慢sql,针对这些慢sql查询做特定的索引优化。 关于慢sql查询不清楚的可以参考这篇文章:https://blog.csdn.net/qq_40884473/article/details/89455740

sql优化总结

1、了解b+tree的数据结构。所用的索引优化都建立在b+tree的基础上
2、 表的数据量尽量不要太大,太大的情况可以考虑分库分表。怎么才算大可以参考阿里巴巴的开发手册。数据量大于500万或者容量大于2G。
3、根据上面的索引设计原则为sql设计合适的索引。尽量以两到三个联合索引,覆盖80%的sql查询。
4、如果查询的数据量过大,一般情况下是不会走辅助索引(二级索引)的。因为辅助索引查出来的数据都要进行回表。因此尽量使用到覆盖索引,直接能从辅助索引中获取到查询的数据。
5、in和or在不同的情况下,有可能走索引,有可能不走。需要在实际环境中通过explain进行分析。
6、like查询如果%在开头会导致索引失效,在中间和后面就不会。如果非得要在前面,可以建立覆盖索引。如果建不了,可以使用搜索引擎。
7、Order by与Group by优化 尽量避免文件排序,尽量使用到索引排序。如果Order by与Group by优化的时候和where产生冲突,那么优先解决where,因为where筛选的数据量小了,排序压力自然就小了。
8、分页优化。具体看分页优化那部分吧。
9、count(*)查询优化,mysql内部专门做了优化,不会取出全部的值,而是按行累加,效率很高,所以不需要用 count(列名)或count(常量)来替代 count(*)。
10 join优化。在关联字段上用到索引,尽量让mysql采用NLJ算法。使用left join 和right join的时候基于小表驱动大表。至于小表并不是数据量小的表就是小表,而是通过where筛选后的数据量小的表才是小表。

你可能感兴趣的:(性能优化,mysql,数据库,sql)