开发人员MySQL调优-实战篇2-让SQL使用索引详解

建议先看看开发人员MySQL调优-实战篇0

让执行的SQL使用索引

虽然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工具简介

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工具使用熟练,并且有多年的经验,真的该如何去分析。新手拿到日志文件,并且知道每一个参数的意思,可能短期也拿不出比较切实有效的统计结果

相关文章
相关标签/搜索