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

て烟熏妆下的殇ゞ 提交于 2020-03-02 07:34:12

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!