虽然DBA给咱们建了不少索引,但没有经验的开发人员每每只看表结构,不太关注索引和如何利用索引提升SQL执行速度,下面罗列一些经验,让你写的SQL更加高效的利用索引mysql
在作实验以前最好先想想索引的数据结构与排序,以及索引工做的方式,才能更快的理解与记住这些点,不然在工做中遇到更复杂的状况,极可能就不会处理了正则表达式
查询字段与某一个索引的结构彻底一致,包括字段和顺序(彻底匹配)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函数,看看执行计划oracle
变全表扫描了socket
若是在实际生产中,确实须要经过函数来处理列再查询怎么办?函数
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如下版本oop
现有表上冗余一个字段,浪费存储,最重要的是还得修改之前的代码
用触发器往另一张表同步数据,使整个事务的流程更长,增长了不稳定性
查询时索引中的某一列已是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参数
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工具使用熟练,并且有多年的经验,真的该如何去分析。新手拿到日志文件,并且知道每一个参数的意思,可能短期也拿不出比较切实有效的统计结果