MySQL(五)查询优化

文章目录

  • 前言
  • 查询慢的原因
  • 优化数据访问
    • 减少访问数据量(扫描行)
    • 是否向数据库请求了不需要的数据
  • 执行过程的优化(查询)
    • 语法解析器和预处理
    • 查询优化器
      • last_query_cost
      • 在很多情况下mysql会选择错误的执行计划,原因如下:
    • 优化器的优化策略
    • 优化器的优化类型
    • 关联查询
      • join的实现方式原理
      • 案例演示
    • 排序优化
  • 优化特定类型的查询
    • 优化count()查询
    • 优化关联查询
    • 优化子查询
    • 优化limit分页
    • 优化union查询
    • 用户自定义变量
      • 自定义变量的使用
      • 自定义变量的限制
      • 自定义变量的使用案例

前言

如果表里数据不多的话,就无所谓优化啦,优化前后差别不大;一旦表中的数据上了规模,查询慢的情况应该就经常发生啦.这也是我们要解决的,最大的问题.

查询慢的原因

  • 网络
  • CPU
  • IO
  • 上下文切换
    n多个任务并发执行,就会有上下文切换,比较浪费时间
  • 系统调用
  • 生成统计信息
    show profile,performance_schema等
  • 锁等待时间
    并发场景下,锁会非常复杂 (读写锁,行表锁)

优化数据访问

减少访问数据量(扫描行)

查询性能低下的主要原因是访问的数据太多,某些查询不可避免的需要筛选大量的数据,我们可以通过减少访问数据量的方式进行优化:

如果查询的数据量很大,可能就不走索引,而是走filesort,因为MySQL认为数据量大时走索引效率很低

  1. 确认应用程序是否在检索大量超过需要的数据
  2. 确认mysql服务器层是否在分析大量超过需要的数据行
    举例,分页时,看一下执行计划,大概扫了多少行数据:
-- 常见的分页,查看执行计划,rows几乎是全表扫描了一遍;
-- 得出结论:如果limit需要跳过的行很多,MySQL会进行全表扫描
explain select * from rental limit 1000000,5;

-- 可以用子查询优化(这两种差距倒不是很大,但是也是一种优化手段了;数据量越大,越明显):
select a.* from rental a inner join (select rental_id from rental limit 1000000,5) b on a.rental_id = b.rental_id ;

是否向数据库请求了不需要的数据

  • 查询不需要的记录
    我们常常会误以为mysql会只返回需要的数据,实际上mysql却是先返回全部结果再进行计算,在日常的开发习惯中,经常是先用select语句查询大量的结果,然后获取前面的N行后关闭结果集。
    优化方式是在查询后面添加limit
  • 多表关联时返回全部列
select * from actor inner join film_actor using(actor_id) inner join film using(film_id) where film.title='Academy Dinosaur';

select actor.* from actor...;
  • 总是取出全部列
    在公司的企业需求中,禁止使用select *,虽然这种方式能够简化开发,但是会影响查询的性能,所以尽量不要使用
  • 重复查询相同的数据
    如果需要不断的重复执行相同的查询,且每次返回完全相同的数据,因此,基于这样的应用场景,我们可以将这部分数据缓存起来,这样的话能够提高查询效率

执行过程的优化(查询)

在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中查询缓存中的数据,如果查询恰好命中了查询缓存,那么会在返回结果之前会检查用户权限,如果权限没有问题,那么mysql会跳过所有的阶段,就直接从缓存中拿到结果并返回给客户端
MySQL8已经干掉缓存模块了

MySQL查询(查询完缓存之后)会经过以下几个步骤:解析SQL、预处理、优化SQL执行计划,这几个步骤出现任何的错误,都可能会终止查询

语法解析器和预处理

mysql通过关键字将SQL语句进行解析,并生成一颗解析树(AST抽象语法树),mysql解析器将使用mysql语法规则验证和解析查询,例如验证使用使用了错误的关键字或者顺序是否正确等等,预处理器会进一步检查解析树是否合法,例如表名和列名是否存在,是否有歧义,还会验证权限等等

apache calcite开源SQL解析工具

查询优化器

当语法树没有问题之后,相应的要由优化器将其转成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的执行计划

mysql使用的是基于成本的优化器,在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个

last_query_cost

select count(*) from film_actor;
show status like ‘last_query_cost’;
可以看到这条查询语句大概需要做1104个数据页才能找到对应的数据,这是经过一系列的统计信息计算来的:

  1. 每个表或者索引的页个数
  2. 索引的基数
  3. 索引和数据行的长度
  4. 索引的分布情况

在很多情况下mysql会选择错误的执行计划,原因如下:

  1. 统计信息不准确
    InnoDB因为其mvcc的架构,并不能维护一个数据表的行数的精确统计信息
    DV,cardinality.HyperLogLog等,都不是准确数,而是考虑一个"量级"
  2. 执行计划的成本估算不等同于实际执行的成本
    有时候某个执行计划虽然需要读取更多的页面,但是他的成本却更小,因为如果这些页面都是顺序读或者这些页面都已经在内存中的话,那么它的访问成本将很小,mysql层面并不知道哪些页面在内存中,哪些在磁盘,所以查询之际执行过程中到底需要多少次IO是无法得知的
  3. mysql的最优可能跟你想的不一样
    mysql的优化是基于成本模型的优化,但是有可能不是最快的优化
    执行表关联时,MySQL可能会调整连接的顺序.
  4. mysql不考虑其他并发执行的查询
  5. mysql不会考虑不受其控制的操作成本
    执行存储过程或者用户自定义函数的成本

优化器的优化策略

静态优化: 直接对解析树进行分析,并完成优化
动态优化: 动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关
mysql对查询的静态优化只需要一次,但对动态优化在每次执行时都需要重新评估

优化器的优化类型

  • 重新定义关联表的顺序
    数据表的关联并不总是按照在查询中指定的顺序进行,决定关联顺序时优化器很重要的功能
    用straight_join可以告诉MySQL,不要帮忙调整顺序

  • 将外连接转化成内连接,内连接的效率要高于外连接

  • 使用等价变换规则,mysql可以使用一些等价变化来简化并规划表达式
    比如 a != 4 和 a<4 and a>4 看他俩谁效率高

  • 优化count(),min(),max()
    索引和列是否可以为空通常可以帮助mysql优化这类表达式:例如,要找到某一列的最小值,只需要查询索引的最左端的记录即可,不需要全文扫描比较

  • 预估并转化为常数表达式
    当mysql检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行处理
    explain select film.film_id,film_actor.actor_id from film inner join film_actor using(film_id) where film.film_id = 1

  • 索引覆盖扫描,当索引中的列包含所有查询中需要使用的列的时候,可以使用覆盖索引

  • 子查询优化
    mysql在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问,例如将经常查询的数据放入到缓存中

  • 等值传播
    (其实日常SQL中我们已经用了这个了,只是可能不知道这个名词)
    如果两个列的值通过等式关联,那么mysql能够把其中一个列的where条件传递到另一个上:
    explain select film.film_id from film inner join film_actor using(film_id
    ) where film.film_id > 500;
    这里使用film_id字段进行等值关联,film_id这个列不仅适用于film表而且适用于film_actor表
    explain select film.film_id from film inner join film_actor using(film_id
    ) where film.film_id > 500 and film_actor.film_id > 500;

关联查询

mysql的关联查询很重要,但其实关联查询执行的策略比较简单:
mysql对任何关联都执行嵌套循环关联操作,即mysql先在一张表中循环取出单条数据,然后再嵌套到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。
mysql会尝试再最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行之后,mysql返回到上一层次关联表,看是否能够找到更多的匹配记录,以此类推迭代执行。
整体的思路如此,但是要注意实际的执行过程中有多个变种形式:

join的实现方式原理

MySQL(五)查询优化_第1张图片
MySQL(五)查询优化_第2张图片

(1)Join Buffer会缓存所有参与查询的列而不是只有Join的列。
(2)可以通过调整join_buffer_size缓存大小
(3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间。
(4)使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。
show variables like ‘%optimizer_switch%’
show variables like ‘%join_buffer%’;

案例演示

查看不同的顺序执行方式对查询性能的影响:
explain select film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor using(film_id) inner join actor using(actor_id);

发现并不是我们写的SQL的表的顺序

可以强制按照自己预想的规定顺序执行:
explain select straight_join film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor using(film_id) inner join actor using(actor_id);

发现人家MySQL的优化是有道理的,强制按照这个顺序的话,需要读取更多的行

查看执行的成本:
show status like ‘last_query_cost’;

排序优化

无论如何排序都是一个成本很高的操作,所以从性能的角度出发,应该尽可能避免排序或者尽可能避免对大量数据进行排序。
推荐使用利用索引进行排序,但是当不能使用索引的时候,mysql就需要自己进行排序,如果数据量小则再内存中进行,如果数据量大就需要使用磁盘,mysql中称之为filesort。
如果需要排序的数据量小于排序缓冲区(show variables like ‘%sort_buffer_size%’,mysql使用内存进行快速排序操作,如果内存不够排序,那么mysql就会先将树分块,对每个独立的块使用快速排序进行排序,并将各个块的排序结果存放再磁盘上,然后将各个排好序的块进行合并,最后返回排序结果.

排序的算法:

  • 两次传输排序
    第一次数据读取是将需要排序的字段读取出来,然后进行排序,第二次是将排好序的结果按照需要去读取数据行。
    这种方式效率比较低,原因是第二次读取数据的时候因为已经排好序,需要去读取所有记录而此时更多的是随机IO,读取数据成本会比较高
    两次传输的优势,在排序的时候存储尽可能少的数据,让排序缓冲区可以尽可能多的容纳行数来进行排序操作

  • 单次传输排序
    先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果,此方式只需要一次顺序IO读取所有的数据,而无须任何的随机IO,问题在于查询的列特别多的时候,会占用大量的存储空间,无法存储大量的数据

当需要排序的列的总大小超过max_length_for_sort_data定义的字节,mysql会选择双次排序,反之使用单次排序,当然,用户可以设置此参数的值来选择排序的方式

优化特定类型的查询

优化count()查询

count(*), count(1), count(column) ?
explain查看执行计划的话,这仨一毛一样
每次执行完后,使用show status like ‘last_query_count’; 查看消耗时间,也是一样的
要说区别的话,功能上有区别,如果按照某列count,不会统计NULL值

  • 总有人认为myisam的count函数比较快,这是有前提条件的,只有没有任何where条件的count(*)才是比较快的
    myisam里面有一个变量来记录整体插入数据的行数,所以count(*)比较快;但是 一旦带了where条件,那个值是不准确的,所以它还是会走普通查询

  • 使用近似值
    在某些应用场景中,不需要完全精确的值,可以参考使用近似值来代替,比如可以使用explain来获取近似的值
    其实在很多OLAP的应用中,需要计算某一个列值的基数,有一个计算近似值的算法叫hyperloglog。

  • 索引覆盖
    一般情况下,count()需要扫描大量的行才能获取精确的数据,其实很难优化,在实际操作的时候可以考虑使用索引覆盖扫描,或者增加汇总表,或者增加外部缓存系统。

优化关联查询

  • 确保on或者using子句中的列上有索引,在创建索引的时候就要考虑到关联的顺序
    当表A和表B使用列C关联的时候,如果优化器的关联顺序是B、A,那么就不需要再B表的对应列上建上索引,没有用到的索引只会带来额外的负担,一般情况下来说,只需要在关联顺序中的第二个表的相应列上创建索引
  • 确保任何的groupby和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程

优化子查询

子查询的优化最重要的优化建议是尽可能使用关联查询代替
因为会把子查询的结果放到临时表里面,可能会增加IO

如果是 where xxx in (子查询),这样就不太好改成关联查询,而且子查询的数据量很小的话,也就无所谓了

优化limit分页

在很多应用场景中我们需要将数据进行分页,一般会使用limit加上偏移量的方法实现,同时加上合适的orderby 的子句,如果这种方式有索引的帮助,效率通常不错,否则的化需要进行大量的文件排序操作,还有一种情况,当偏移量非常大的时候,前面的大部分数据都会被抛弃,这样的代价太高。
要优化这种查询的话,要么是在页面中限制分页的数量,要么优化大偏移量的性能

优化此类查询的最简单的办法就是尽可能地使用覆盖索引,而不是查询所有的列
explain select film_id,description from film order by title limit 50,5
explain select film.film_id,film.description from film inner join (select film_id from film order by title limit 50,5) as lim using(film_id);

优化union查询

mysql总是通过创建并填充临时表的方式来执行union查询,因此很多优化策略在union查询中都没法很好的使用。经常需要手工的将where、limit、order by等子句下推到各个子查询中,以便优化器可以充分利用这些条件进行优化.
除非确实需要服务器消除重复的行,否则一定要使用union all,因为没有all关键字,mysql会在查询的时候给临时表加上distinct的关键字,这个操作的代价很高

用户自定义变量

自定义变量的使用

set @i:=1;
select @i;
select @i:=@i+1;
set @min_actor :=(select min(actor_id) from actor)
set @last_week :=current_date-interval 1 week;
用户自定义变量,当前会话有效,退出就没啦.

两个@@表示系统变量,如 select @@autocommit;

自定义变量的限制

1、无法使用查询缓存
2、不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名或者limit子句
3、用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
4、不能显式地声明自定义变量地类型
5、mysql优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想地方式运行
6、赋值符号:=的优先级非常低,所以在使用赋值表达式的时候应该明确的使用括号
7、使用未定义变量不会产生任何语法错误

自定义变量的使用案例

窗口函数?开窗函数?
应用场景: https://zhuanlan.zhihu.com/p/115524915
我是觉得这个没啥用,把核心数据查出来,在Java中做数据处理不是更好嘛,也减少了和数据库的IO
当然,如果是特殊场景,临时写个SQL做数据分析,那这个还是有很大用处的.

优化排名语句:
在给一个变量赋值的同时使用这个变量
select actor_id,@rownum:=@rownum+1 as rownum from actor limit 10;

避免重新查询刚刚更新的数据
当需要高效的更新一条记录的时间戳,同时希望查询当前记录中存放的时间戳是什么
update t1 set lastUpdated=now() where id =1;
select lastUpdated from t1 where id =1;
->
update t1 set lastupdated = now() where id = 1 and @now:=now();
select @now;

确定取值的顺序
在赋值和读取变量的时候可能是在查询的不同阶段
set @rownum:=0;
select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1;
因为where和select在查询的不同阶段执行,所以看到查询到两条记录,这不符合预期

set @rownum:=0;
select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1 order by first_name
当引入了order by之后,发现打印出了全部结果,这是因为order by引入了文件排序,而where条件是在文件排序操作之前取值的

解决这个问题的关键在于让变量的赋值和取值发生在执行查询的同一阶段:
set @rownum:=0;
select actor_id,@rownum as cnt from actor where (@rownum:=@rownum+1)<=1;

你可能感兴趣的:(MySQL)