建议先看看开发人员MySQL调优-实战篇0
让执行的SQL使用索引
虽然DBA给我们建了很多索引,但没有经验的开发人员往往只看表结构,不太关注索引和如何利用索引提高SQL执行速度,下面罗列一些经验,让你写的SQL更加高效的利用索引
在做实验之前最好先想一想索引的数据结构与排序,以及索引工作的方式,才能更快的理解与记住这些点,否则在工作中遇到更复杂的情况,很可能就不会处理了
-
查询字段与某一个索引的结构完全一致,包括字段和顺序(完全匹配)
create index idx_tv_v_user_u_a_g on tb_v_user(user_name,age,gendor); select * from tb_v_user where user_name = '1F7sJ' and age = 44 and gendor = 1;
查询条件与索引的列与顺序完全一致,执行计划扫描类型为ref。有兴趣的同学可以试试分别将user_name、age、gendor查询条件删除,看看执行计划是什么情况
-
不在列上做任何附加操作,比如加函数
select * from tb_v_user where left(user_name,5) = '1F7sJ' and age=44 and gendor = 1;
这里的SQL和上面的SQL执行结果是一模一样,写法上区别是在user_name上使用了left函数,看看执行计划
变全表扫描了
如果在实际生产中,确实需要通过函数来处理列再查询怎么办?
1.如果你的MySQL是5.7以上版本,可以创建一个虚拟的列,然后在该列上创建函数索引。oracle可以直接创建函数索引,比这个简单一些
alter table tb_v_user add column user_name_t varchar as (left(user_name,5)) stored; 或者 alter table tb_v_user add column user_name_t varchar as (left(user_name,5)) virtual; alter table tb_v_user add key idx_tb_v_user_ut(user_name_t);
2.如果你的MySQL是5.7以下版本
现有表上冗余一个字段,浪费存储,最重要的是还得修改以前的代码
用触发器往另外一张表同步数据,使整个事务的流程更长,增加了不稳定性
-
查询时索引中的某一列已经是range扫描了,那么索引中后面列的扫描就无法再使用索引了
select * from tb_v_user where user_name = '1F7sJ' and age > 30 and gendor = 1;
虽然查询条件和索引的列完全一致,但是因为在age使用了range扫描,后面gendor的条件是无法再使用索引的
-
尽量使用覆盖索引
select user_name,age,gendor from tb_v_user where user_name = '1F7sJ' and age = 44 and gendor = 1;
SQL语句只是将*替换成了指定的列,而且这些列都在索引中存在。像这种情况,及时gendor查询条件不能再走索引的扫描,但是数据的读取还是可以走索引的,这样可以减少IO的次数
-
减少使用不等于、is null 、is not null、or,会触发全表扫描
这个应该很明确吧,都属于范围类型的查询,当然没法走索引扫描咯
-
like 使用时优先考虑右则使用通配符,如果最左侧也需要通配符,尽量走索引全扫描(index)
select user_id,user_name,age,gendor from tb_v_user where user_name like '%1F7sJ%';
select user_id,user_name,age,gendor from tb_v_user where user_name like '1F7sJ%';
全表(all)扫描变成了范围(range)扫描
注:虽然like也是范围扫描,但是like毕竟能够最终确认出一个指定的结果集,因此跟在like后面的查询条件是可以用到索引的。这一点和'>'出现的范围扫描不一样
如果需求必须要根据列中间的部分做模糊查询,那么尽量使用索引覆盖,减少IO次数
select user_name,age,gendor from tb_v_user where user_name like '%1F7sJ%';
就是SQL查询的列在索引列中存在
-
如果列是字符串,一定以及千万要加单引号,如果这个都不记得的话,估计你的实习期就过不了
表某些列的值虽然都是阿拉伯数字,但是列类型却是varchar,此时就很容易在写SQL的时候当成数值类型,这样写会导致没办法使用到索引,比如给值加上单引号
-
查询条件+排序+分组的时候,也尽量符合完全匹配原则,特别是排序要紧随查询条件后面的列,否则会出现Using filesort
总结:上面虽然罗列了不少内容,最终概况起来是:利用现有索引,再改变需求的情况下,尽量使用索引覆盖,保持列的类型不变(不加函数、指明类型),减少模糊查询。如果现有索引满足不了,而需求又必须实现,那么就把DBA和需求负责人一起拉过来讨论讨论吧,找一个折中的办法,工作中一般码农和维护人员都会屈服于需求人员的淫威
小表驱动大表
顾名思义就是用数据量小的表与数据量大的表做匹配,减小IO和比较次数。比较常见的三种场景如下,他们三者功能很类似,都可以用来做过滤,但是join通过丰富的关联能实现很多集合操作。详情可以掉头看看我之前的帖子
先准备点数据
create table tb_v_s_user select user_name,count(1) as cnt from tb_v_user group by user_name having COUNT(1)>5; #因为后面的测试是基于user_name做的关联,为了减少IO次数,我使用了索引覆盖优化方式 create index idx_tb_v_s_user_u on tb_v_s_user(user_name);
1.in
select user_name from tb_v_user where user_name in(select user_name from tb_v_s_user);
先从tb_v_s_user的索引中读取出所有的user_name,然后在循环和tb_v_user索引中的user_name做比较(ref扫描),如果我把两个表的顺序换一下会如何
select user_name from tb_v_s_user where user_name in(select user_name from tb_v_user);
看来MySQL的优化器做了自动优化,并没有傻傻的先加载大表tb_v_user。再使用in的时候,MySQL会自动以小表驱动大表的优化策略优化SQL
两种编写方式估算出来的rows都等于94=(93+1)
但我还是推荐第一种写法
2.exists
先将in的第一种写法翻译过来看看
select * from tb_v_user a where exists ( select 1 from tb_v_s_user b where a.user_name = b.user_name);
虽然都用到了索引,但是走了大表的索引全扫描,明显IO的次数比in的第一种写法会多很多很多,效率一定会查很多,这种方式就成了大表驱动小表,当然不是我们想要的结果。接下来翻译in的第二种写法
select a.user_name from tb_v_s_user a where exists ( select 1 from tb_v_user b where a.user_name = b.user_name);
这下就变成了我们想要的结果,小表驱动大表,然后估算出来的rows=94
3.join
大表写在前:
select a.user_name from tb_v_user a join tb_v_s_user b on a.user_name = b.user_name;
小表写在前:
select a.user_name from tb_v_s_user a join tb_v_user b on a.user_name = b.user_name;
可以看出执行计划并没有改变,和用in一样,MySQL还是很智能的。
总结:in exists join三种方式都可以实现过滤,但是如果对原理掌握不是很好的话,可以使用in 和 join,不过现在智能的东西不一定每次都对,因此在实际开发中,最好还是看看执行计划,不要太相信优化器。
排序
调优SQL:
create index idx_tv_v_user_u_a_g on tb_v_user(user_name,age,gendor);
select user_name,age,gendor from tb_v_user where user_name = '1F7sJ' order by age,gendor;
select user_name,age,gendor from tb_v_user where user_name > '1F7sJ' order by age,gendor;
有上面的所有,先想想这两个谁会出现Using filesort,为什么它会出现,另外一个不会出现
答案是第二种有Using filesort出现,第一种没有,为什么喃?因为第一种最终查询结果集user_name肯定全都是1F7sJ,这一列不再需要排序,而第二种user_name是不确定的,且order by 子句与索引列不是前缀匹配(看下图),但是结果集又包含了user_name,所以MySQL没有已排好序的数据可拿,只能再做一次排序。
先来一个简单一点的:
再来一个复杂一点的:
例子1:select user_name from tb_v_user order by user_name[,age,gendor]
列子2:select user_name from tb_v_user order where user_name = '1F7sJ' order by user_name[,age,gendor]
它们两个都不会产生Using filesort
如果上面的还不能理解,希望下面的图(来自于尚硅谷-周阳)可以有用
注意:这里查询的列都指定为了user_name而不是'*'。如果改成'*'可能就演示不出来效果
调优参数:
1.如果查询语句有order by ,千万小心使用select *
2.联系DBA调整sort_buffer_size参数
3.联系DBA调整max_length_for_sort_data
虽然可以调大缓冲区的大小,但是针对大表,而又不可避免对大数据量的排序,而且还是filesort,遇到这种情况,建议在系统负载比较清闲的时间段做,或者干脆去离线的大数据方式解决
分组
首先得先明白什么是分组,如果不懂的,出门左转百度一下数据库的分组是什么意思,分组之后数据长什么样,可以做哪些统计操作等。
分组其实就是先排序,然后对分组字段做统计操作,所以要优化分组,必须得先优化排序。因此排序的优化策略是完全适用的,不同的是要减少写分组过滤条件(having),能用where代替的就使用where,比较 having 后面跟的都是统计函数
慢查询日志
虽然我们在测试环境对每个SQL的执行计划都做过验证,但是并不代表在生产环境MySQL就乖乖的按照测试环境的执行计划来跑,因为数据量完全不一样。所以当有新需求上线、数据割接应该打开MySQL的慢查询日志(一般数据量大的系统还会定期做慢查询日志分析),收集查询时间大于long_query_time的SQL
查看慢查询日志相关参数
show variables like '%slow_query_log%'; +------------------------------------+----------------------------------+ | Variable_name | Value | +------------------------------------+----------------------------------+ | slow_query_log | OFF | | slow_query_log_always_write_time | 10.000000 | | slow_query_log_file | /var/lib/mysql/hadoop00-slow.log | | slow_query_log_timestamp_always | OFF | | slow_query_log_timestamp_precision | second | | slow_query_log_use_global_control | | +------------------------------------+----------------------------------+ 6 rows in set
查看慢查询日志记录数
mysql> show global status like '%Slow_queries%' +---------------+-------+ | Variable_name | Value | +---------------+-------+ | Slow_queries | 1 | +---------------+-------+ 1 row in set
所以如果执行这个命令看到value很大,那么你的系统一定是很慢的
临时设置:
下面的设置只对当前会话有效,时间单位是秒
set global slow_query_log=1; set long_query_time=3; select user_id,count(1) from tb_vote group by user_id;
到主机目录下查看有没有将SQL打印出来
[root@hadoop00 /var/lib/mysql]$ cat hadoop00-slow.log /usr/sbin/mysqld, Version: 5.6.24-72.2 (Percona Server (GPL), Release 72.2, Revision 8d0f85b). started with: Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock Time Id Command Argument # Time: 180612 7:19:47 # User@Host: root[root] @ [192.168.245.1] Id: 10 # Schema: mydb Last_errno: 0 Killed: 0 # Query_time: 3.603351 Lock_time: 0.000180 Rows_sent: 400000 Rows_examined: 1200000 Rows_affected: 0 # Bytes_sent: 10800115 use mydb; SET timestamp=1528813187; select user_id,count(1) from tb_vote group by user_id;
确实打印出来了
永久设置:
打开/etc/my.cnf文件,添加如下配置,时间单位是秒
#还可以slow_query_log=true slow_query_log=1 long_query_time=3 #还可以设置日志文件目录和名称 slow_query_log_file=/home/hadoop/data/mysql/mysql-slow.log
一般开发人员是没有权限去修改这些的,需要联系DBA配合,而且长时间打开这对性能也是有一定影响的
DBA或者项目经理可以借助MySQL提供的mysqldumpslow工具对慢查询日志做分析
mysqldumpslow工具简介
mysqldumpslow参数
s:按那种方式排序
c:访问次数
l:锁定时间
r:返回记录
t:查询时间
al:平均锁定时间
ar:平均返回记录数
at:平均查询时间
t:返回前面多少条的数据
g:后半搭配一个正则表达式,忽略大小写
举例:
1.得到返回记录最多的10个SQL
mysqldumpslow -s r -t 10 /home/hadoop/data/mysql/1.log
2.得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /home/hadoop/data/mysql/2.log
3.得到按照时间排序的前10条含有左连接的SQL
mysqldumpslow -s t -t 10 -g "left join" /home/hadoop/data/mysql/3.log
4.还可以结果管道(|)和more使用
mysqldumpslow -s r -t 10 /home/hadoop/data/mysql/4.log | more
这个确实很简单,对于一般开发人员估计不会让你干这种事情,都是DBA和项目经理来干,需要对mysqldumpslow工具使用熟练,而且有多年的经验,真的该如何去分析。新手拿到日志文件,而且知道每个参数的意思,可能短时间也拿不出比较切实有效的统计结果
来源:oschina
链接:https://my.oschina.net/u/3049601/blog/1829214