MySQL——查询性能优化

一、为什么查询速度会慢

通常来说:查询生命周期大致可以按顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划、执行,并返回结果给客户端

执行可以认为是整个生命周期中最重要的阶段,包括大量的检索数据到存储引擎的调用以及调用后的数据处理(排序和分组等)。

二、慢查询基础:优化数据访问

查询性能低下最基本的原因是访问的数据太多。

① 确认应用程序是否在搜索大量超过需要的数据。

② 确认MySQL服务器层是否在分析大量超过需要的数据行。

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

① 查询不需要的记录

-- 若页面上只有需要10行数据  返回100行甚至更多是得不偿失的 最简单方法  limit
select <cols> from table where col_name = 'col_val'  limit 10;

常见的错误认识:MySQL只返回需要的结果集,实际上MySQL是先返回全部结果集再进行运算的,

-- 以前公司分页框架 查询count时
select count(*) fromselect <cols> from table where ...)
-- 这个时候MySQL会查询出全部<cols>然后进行count处理,查询效率很低,可优化为
select count(*) from table where ...

② 总是取出全部的列+多表关联时返回全部的列

查询中应该拒绝使用 '*'

select * from table where ...
-- * 会查询所有的列,平常学习或者解决问题时用*没有问题,但不要在生产应用中使用*
-- 特别是多表关联时  会查询出所有的表的所有列 数据量巨大

③ 避免重复查询相同的数据

热点数据,最好使用缓存解决。避免重复不断地查询相同的数据。

2、MySQL是否扫描了额外的记录

对于MySQL,最简单的衡量查询的指标:响应时间、扫描的行数、返回的行数。

① 响应时间

在日常开发中,需要重视响应时间。一般公司都会周期统计响应时间过长的SQL。响应时间=服务时间+排队时间。

② 扫描的行数与返回的行数

分析查询时:查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。理想情况下扫描的行数=返回的行数,但实际操作过程中多表联合查询出一行数据是常态。

-- 注意explain sql 返回列表中的rows属性:扫描的行数

③ 扫描的行数和访问类型

-- explain sql 返回列表中的type属性:访问类型
-- 速度从慢到快 扫描的行数也是从小到大
-- all :全表扫描
-- index :索引扫描
-- range :范围扫描 一般要求最少要达到range
-- ref :非唯一索引扫描
-- eq_ref:唯一索引扫描
-- cons :常熟引用

④ 一般MySQL能够使用三种方式应用where条件(extra : using where),从好到坏依次是:

a 在索引中使用where条件过滤不匹配的记录。这是在MySQL存储引擎层完成的

b 使用索引覆盖(覆盖索引)扫描来返回记录(extra :using index),直接从索引中过滤不需要的记录并返回命中的结果。这是由MySQL服务器层完成的。

c 从数据表中返回数据,然后过滤不满足条件的记录(extra : using where)。这也是在MySQL服务器层完成的。

select actor_id,count(1) from sakila.film_actor group by actor_id;
-- 误区:应用where条件不是sql中的where
-- 当actor_id是二级索引 对应a
-- 当actor_id是主键索引(聚簇)对应b
-- 当actor_id不是索引    对应c

⑤ 查询需要扫描大量的数据只返回少量的行,通常有下列技巧优化

a 使用索引覆盖扫描,把所有需要返回的列都放到索引中,存储引擎无须回表获取对应的行就可以返回数据了。

b 改变库表结构。使用单独的汇总表

c 重写这个复杂的查询

三、重构查询的方式

 1、一个复杂查询还是多个简单查询

设计查询的一个重要问题:是否需要将一个复杂的查询分为多个简单的查询

传统实现中:总是强调需要数据层完成尽可能多的工作,这样做的逻辑在以前总是认为网络通信、查询解析和优化是一件代价很高的事情。

MySQL:MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。并且现在网络速度比以前要快的多。

实际设计中一个大查询分解成多个小查询是很有必要的。不要害怕这样做,但也要衡量一下开发成本,这样是不是会减少工作量。例如:查询一个很小的表,完全没必要拆分开来。

2、切分查询

有时候对于一个大查询我们需要“分而治之”,将大查询切分为小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果

例如:删除旧数据。定期地清理大量数据时,如果用一个大的语句一次性完成的话,可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。

这个时候我们可以将delete语句切分为多个较小的查询,尽可能小的影响MySQL的性能。

delete from messages where created < date_sub(now(),interval 3 month);
-- 可分治为下面逻辑
delete from messages where created < date_sub(now(),interval 3 month) limit 10000;
-- 然后自定义执行时间,多执行几次,例如一个小时一次,一天就是24w数据

3、分解关联查询

很多高性能的应用都会对关联查询进行分解。例如:

select * from tag
join tag_post on tag_post.tage_id=tag.id
join post on tag_host.post_id=post.id
where tag.tag='mysql';
-- 可以分解为
select * from tag where tag='mysql';
select * from tag_post where tag_id= 1234;
select * from post where post.id in (123,456,567,9098,8904);

这样重构的优点:

① 让缓存的效率更高,缓存多张表的数据。

② 将查询分解后,执行单个查询可以减少锁的竞争

③ 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。

④ 查询本身效率也可能有所提升。例如控制in()中id顺序查询比随机的关联要更高效。

⑤ 可以减少冗余记录的查询。

⑥ 更进一步,这样做相当于在应用中实现了hash关联,而不是使用MySQL的嵌套循环关联。

四、查询执行的基础

MySQL查询过程:

 1、客户端/服务器通信协议

① “半双工”通信协议:让MySQL通信简单快速,但有一个明显的限制无法进行流量的控制。

② 对于一个MySQL连接或者说一个线程,任何时候都有一个状态,这个状态表示MySQL当前正在做什么。

-- 由于“半双工”通信协议。了解这些状态含义,可以很快了解处理进度。
-- Sleep  :线程正在等待客户端发送新的请求
-- Query :线程正在执行查询或者正在将结果发送给客户端
-- Locked :在MySQL服务器层,该线程正在等待表锁。
-- Analyzing and statistics :线程正在收集存储引擎的统计信息,并生成查询执行计划
-- Coping to tmp table [on disk] :线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是在做group by要么order by 或者是 union操作。
-- Sorting result :线程正在对结果集进行排序。
-- Sending data :这表示多种情况:线程可能在多个状态之间传送数据;生成结果集;在向客户端返回数据

2、查询缓存

在解析一个查询语句之前,如果查询缓存时打开的,MySQL会优先检查这个查询是否命中查询缓存中的数据。

① 这个检查是通过对一个大小写敏感的哈希查找实现的,查询和缓存查询即时一个字节不同,也视作为未命中

② 查询命中缓存后,MySQL还会检查用户权限。

3、查询优化处理

① 语法解析器和预处理:MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”,并且对解析树进行语法规则检查。

一个简单的语法树模型,MySQL中肯定会更复杂

② 查询优化器 :当语法树被认为是合法的,优化器会从很多种执行方式,找到最好的执行计划。

select sql_no_cache count(*) from sakila.film_actor;
-- 5462
show status like 'last_query_cost'
-- 1040.599000
-- 结果显示MySQL优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询。

一下原因可能会导致优化器选择错误的执行计划:

a 统计信息不准确。

b 执行计划中的成本估算不等同于实际执行的成本

c MySQL的最优可能和你想的最优不一样。时间短 != 执行成本

d MySQL不考虑其他并发执行的查询

e MySQL不是任何时候都是基于成本的优化。又是也会基于一些固定的规则。

f MySQL不会考虑不受其控制的操作的成本

g 优化器有时无法估算所有可能的执行计划,而错过实际最优执行计划

MySQL能够处理的优化类型:

重新定义关联表的顺序:数据表关联并不总是按照查询中指定顺序进行的。

将外连接转化成内连接:并不是所有outer join语句都必须以外连接方式执行。例如where条件、库表结构可能使外连接等价于一个内连接。

使用等价变换规则:MySQL可以使用一些等价变化来简化规范表达式。(5 = 5 and a > 5)改写为 a > 5

优化count()min()和max():索引和列是否可为空通常可以帮助MySQL优化这类表达式。

预估并转化为常数表达式:当MySQL检测到一个表达式可以转化为常数的时候。就会一直把该表达式作为常数进行优化处理

覆盖索引扫描:当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据。

子查询优化:MySQL在某些情况下可以将子查询转换一种效率更高的形式。

提前终止查询:在发现以满足查询需求的时候,MySQL总是能够立刻终止查询。limit

等值传播:如果两个列的值通过等式关联,那么MySQL能够把其中一个列的where条件传到另一列上。

列表IN()的比较:在有些数据库系统中in()等同于or,但MySQL不是,in()列表中数据先排序,然后二分查找方式确定列表中值是否满足条件。or=O(n)则in=O(logn)

③ 数据和索引的统计信息

在服务层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现。

④ MySQL如何执行关联查询

MySQL对任何关联都执行嵌套循环关联操作,MySQL先在一个表中循环取出单条数据,然后嵌套循环到下一个表中寻找匹配的行,依次直至执行完毕

⑤ 执行计划

与很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询,生成查询的一棵指令树。

⑥ 关联查询优化器

MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。

⑦ 排序优化

无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。

4、查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划完成整个查询。

5、MySQL查询优化器的局限性

① 关联子查询:MySQL的子查询实现得非常糟糕。最糟糕的一类查询时where条件中包含in()的子查询语句。

select * from sakila.film where film_id in (
select film_id from sakila.film_actor where actor_id = 1);
-- 我们通常以为会这样执行
select * from sakila.film where film_id in (1,23,25,106...);
--  实际并非如此
select * from sakila.film where exists (
select * from sakila.film_actor where actor_id = 1
and film_actor.film_id = film.film_id);
-- 很容易重写这个查询
select film.* from sakila.film
inner join sakila.film_actor using(film_id)
where actor_id = 1;

② UNION的限制

③ 索引合并优化

④ 等值传递

⑤ 并行执行

⑥ 哈希关联

⑦ 松散索引扫描

⑧ 最大值和最小值优化

⑨ 在同一个表上查询和更新

五、优化特定类型的查询

 一、优化count()查询

count()是一个特殊的函数,有两种非常不同的作用:

统计某个列值的数量:在count()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值(不为NULL)的结果数。

统计行数:count(*)中*并不会像我们猜想的那样扩展成所有的列count(*) = count(1)在统计行数时最好使用count(*),这样写意义清晰,性能也很好。

select count(*) from world.city where id > 5;
-- 反转一下
select (select count(*) from worldl.city) - count(*) from world.city where id <= 5;

select sum(if(color = 'blue',1,0)) as blue,sum(if(color = 'red',1,0)) as red from items;
select count(color = 'blue' or null) as blue,count(color = 'red' or null) as red from items;

二、优化关联查询

① 确保on 或者using子句中的列上索引。

② 确保任何group by和order by中表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程

③ 当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。

三、优化子查询

① 尽可能的使用关联查询替代子查询。

四、优化group by和distinct

① 索引优化是最有效的方法。

② 无法使用索引时,group by使用两种策略来完成:使用临时表或者文件排序来做分组。

select actor.first_name, actor.last_name,count(*) from sakila.film_actor
    inner join sakila.actor using(actor_id) group by actor.first_name, actor.last_name;
-- 采用查找表的表示列分组的效率会比其他列更高
select actor.first_name, actor.last_name,count(*) from sakila.film_actor
    inner join sakila.actor using(actor_id) group by film_actor.actor_id;

五、优化limit分页

当偏移量很大时,例如limit 10000,20 查询出10020条记录后只返回20条。代价非常高。

可以使用索引覆盖扫描,而不是查询所有的列。

select film_id,description from sakila.film order by title limit 50,5;
-- 延迟关联将大大提高查询效率
select film.film_id,film.description from sakila.film
    inner join (
        select film_id from sakila.film order by title limit 50,5
    ) as lim using(film_id);
-- 或者根据返回film_id值
select film_id,description from sakila.film where film_id > ? order by title limit 5;

六、优化sql_calc_found_rows

分页的时候,一个更好的技巧是

① 具体页数换成下一页按钮,每页显示20条记录,那么可以每次查询21条记录,仅显示20条,第21条数据用作下一页按钮的判断

② 先获取并缓存较多的数据,例如缓存1000条数据,每次分页从缓存中取数据,如果结果集大于1000,则可以页面上设计一个额外的“找到的结果多于1000条”

七、优化union查询

MySQL总是通过创建并填充临时表的方式来执行union查询。因此很多优化策略在union查询中都没法很好地使用。

除非确实需要服务器消除重复的行,否则一定要使用union all。

八、静态查询分析

九、用户自定义变量

 

参考《高性能MySQL》

posted on 2020-02-25 06:04  FFStayF  阅读(411)  评论(0编辑  收藏  举报