Post

SQL调优的流程

SQL调优的流程

SQL调优无论在实际工作中还是在面试过程中都是考查的重点,在文章中我将从慢查询分析和索引两个角度来介绍SQL调优的一点思路。

慢查询分析

SQL优化并不是漫无目的一条条去 explain SQL语句,需要具备针对性,所以我们只需要对执行时间超出我们预期的SQL进行针对的调整,那么慢查询分析日志就是帮我们快速定位超预期执行时间的SQL的一个有力工具。

打开慢查询分析日志开关

打开慢查询分析日志的开关的两种方式:1.修改配置文件,2.设置参数。我们下面使用设置参数的方式来演示:

  • 登入MySQL(略)

  • 查看慢查询日志分析文件的配置信息:

1
show variables like 'slow_query%';
  • 得到如下的配置信息:
1
2
3
4
5
6
7
+---------------------+-------------------------------------------------+
| Variable_name       | Value                                           |
+---------------------+-------------------------------------------------+
| slow_query_log      | OFF                                             |
| slow_query_log_file | /usr/local/mysql/data/TechBird-Macbook-slow.log |
+---------------------+-------------------------------------------------+
2 rows in set (0.00 sec)

slow_query_log 的值 OFF 设置为 ON,即打开慢查询分析日志记录。记录的日志文件会保存在slow_query_log_file 的 Value 所指向的路径文件中,如果需要也可以对其进行修改。

1
set global slow_query_log = 'ON';

更改慢查询分析的默认时间

MySQL默认是的时间是 10 sec,显然时间太长了,我们可以修改这个参数为自己的心里预期时间:

查看默认时间配置:

1
show variables like 'long_query%';

得到如下的配置信息:

1
2
3
4
5
6
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)

修改默认的时间(单位:秒),一般在项目中可以设置为0.1秒,即100毫秒。为了演示方便下面将其设置为1秒:

1
set global long_query_time = 1;

修改完所有参数之后要quit;重新登入数据库才能保证已设置的参数生效。

构造一个慢的SQL测试语句

1
select sleep(3);

查看慢查询分析的日志文件

温馨提示:在查看慢查询分析文件之前要quit; MySQL。

通过查看慢查询分析的日志文件,我们可以了解项目中哪些SQL超出了我们设置的预期执行时间,然后针对其进行优化:

tail /var/lib/mysql/TechBird-Macbook-slow.log

日志如下:

1
2
3
4
5
# Time: 2021-04-01T06:59:15.149853Z
# User@Host: root[root] @ localhost [] Id:  419
# Query_time: 3.005275 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1617260355;
select sleep(3);

通过查看日志,可以准确的定位出是这条select sleep(3);执行超出了预期时间,从而利用下文所介绍的方法针对其进行优化。

索引

我们已经定位出了执行超出预期时间的SQL语句,一般来说可以使用explain工具对其进行分析定位问题,总结起来就是如下几点:

  1. 理解 MySQL InnoDB 的索引原理

  2. 掌握 MySQL 执行计划的方法

  3. 掌握导致索引失效的常见情况

  4. 掌握实际工作中常用的建立高效索引的技巧(如前缀索引、建立覆盖索引等)

在这之前我们需要先介绍一下索引相关的概念和知识。

相关概念

在实际应用中,InnoDB 是 MySQL 建表时默认的存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型。

在创建表时,InnoDB 存储引擎默认使用表的主键作为主键索引,该主键索引就是聚簇索引(Clustered Index),如果表没有定义主键,InnoDB 就自己产生一个隐藏的 6 个字节的主键 ID 值作为主键索引,而创建的主键索引默认使用的是 B+Tree 索引

  1. 索引分类:普通索引唯一索引、全文索引、空间索引
    • 在MySQL的官方文档中,create [option] index语法中,[option]就是用来修饰索引。
    • 缺省代表普通索引
    • unique代表唯一索引
    • fulltext代表全文索引,全文索引目前通常采用ElasticSearch来实现,而不是在数据库内部来做
    • spatial代表空间索引,而空间索引在一些老的MySQL版本中是不支持的,一般用于存储一些地址坐标数据
  2. 存储方式:B-Tree、Hash
    • InnoDB支持B-Tree,不支持Hash
    • InnoDB 选择 B+Tree 当默认的索引数据结构
    • 数据存储有序(最左前缀与此息息相关)
    • 每个叶子节点到根的距离相同
  3. 依赖列数:单列索引、组合索引

  4. 数据分布:聚簇索引二级(辅助)索引

    • 所谓聚簇索引就是指实际的数据行直接存到了叶子节点当中,而主键索引是聚簇索引的一个通常的形式。
    • 通过主键查询数据,不需要回表,直接就可以得到数据行,效率较高。
    • 非聚簇(主键)索引(辅助索引),叶子节点保存的不是最终的数据行,而只是一个主键值,需要通过主键值再去主键(聚簇)索引里查询最终叶子节点的数据行。因此需要走两次索引,是索引的索引,所以称为二级索引或辅助索引。这个过程叫回表
  5. 回表情况:覆盖索引

    • 当一个索引包含(覆盖)了需要查询的字段值时,称其为覆盖索引

    • 只有select、where中出现的列被索引覆盖的情况才是覆盖索引
    • 此时如果使用 explain 分析可以看到 Extra 的值为Using index
    1
    2
    3
    
    colums: a b c d e
    index(a,b,c)
    select a,b,c from T where a="" and b="";
    

最左前缀(Leftmost Prefixing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
table T,index(a,b,c)

-- 全值匹配
select * from T where a='' and b='' and c=''; -- YES
select * from T where c='' and b='' and a=''; -- YES

-- 匹配左前缀
select * from T where a=''; -- YES
select * from T where b=''; -- NO

-- 匹配列前缀
select * from T where a like 'x%'; -- YES
select * from T where a like '%x'; -- NO
select * from T where b like 'x%'; -- NO

-- 匹配范围值
select * from T where a between '' and ''; -- YES
select * from T where b between '' and ''; -- NO

-- 全值匹配 + 范围值匹配
select * from T where a='' and b between '' and ''; -- YES
select * from T where b='' and c between '' and ''; -- NO
select * from T where a between '' and '' and b=''; -- NO

执行计划(explain)

了解完上面的概念之后,我们来介绍一下在实际工作中如何查看索引的执行计划。

我这里有一张存储用户信息的演示表 tab_user:

1
2
3
4
5
6
7
8
9
10
create table `tab_user`
(
   `uid`                 int(11) not null,
   `username`            varchar(255) default null,
   `password`            varchar(255) default null,
   `name`                varchar(255) default null,   
    primary key (`uid`) using btree,
	  key 'index_name' ('name'),
    key 'index_uid_name' ('uid','name')
);

表中包含了主键索引、name 字段上的普通索引,以及 uid 和 name 两个字段的联合索引。现在我们来看一条简单查询语句的执行计划:

1
explain select uid,name from tab_user where uid='1';

explain-sql

对于执行计划,参数有 possible_keys 字段表示可能用到的索引,key 字段表示实际用的索引,key_len 表示索引的长度,rows 表示扫描的数据行数,filtered表示覆盖率%。Extra表示额外的信息说明。

这其中需要重点关注type字段,表示数据扫描类型,也就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为(考虑到查询效率问题,全表扫描和全索引扫描要尽量避免):

  • ALL(全表扫描);

  • index(全索引扫描);

  • range(索引范围扫描);

  • ref(非唯一索引扫描);

  • eq_ref(唯一索引扫描);

  • const(结果只有一条的主键或唯一索引扫描)。

总的来说,执行计划是研发工程师分析索引详情必会的技能(很多大厂公司招聘 JD 上写着“SQL 语句调优” ),所以你在面试时也要知道执行计划核心参数的含义,如 type。在回答时,也要以重点参数为切入点,再扩展到其他参数,然后再说自己是怎么做 SQL 优化工作的(讲个故事)。

索引失效的常见情况

在工作中,我们经常会碰到 SQL 语句不适用已有索引的情况,来看一个索引失效的例子:

1
select * from tab_user where name like '%z'

这条带有 like 查询的 SQL 语句,没有用到 tab_user 表中的 index_name 索引。

explain-sql2

我们结合普通索引的B+Tree结构来分析一下索引失效的原因:当MySQL优化器根据name like ‘%z’这个条件,到索引index_name的B+Tree结构上去查询评估时,发现当前节点的左右子节点的值都有可能符合’%z’这个条件,于是优化器判定当前索引需要扫描整个索引,而且还要回表查询,不如直接全表扫描。

当然还有其他类似的索引失效的情况:

  1. 索引列上做了表达式计算、函数、隐式类型转换操作,这些情况下索引失效是因为查询过程需要扫描整个索引并回表,代价高于直接全表扫描;
  2. like匹配使用了左模糊查询(’%abc’)或左右模糊查询(’%abc%’);
  3. 字符串不加引号导致隐式类型转换;
  4. 被使用的索引字段不是联合索引的最左字段

总结:如果MySQL查询优化器预估走索引的代价比全表扫描的代价还要大,则不走对应的索引,直接扫描全表。如果走索引比全表扫描代价小,则使用索引。

常见优化索引的方法

  • 前缀索引优化

    • 前缀索引就是用某个字段中,字符串的前几个字符建立索引;使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

    • 但是前缀索引有一定的局限性,例如 order by 就无法使用前缀索引,无法把前缀索引用作覆盖索引。

  • 覆盖索引优化

    • 覆盖索引是指 SQL 中 query 的所有字段,在索引 B+ Tree 的叶子节点上都能找得到的那些索引,从辅助索引中查询得到记录,而不需要通过聚簇索引查询获得。

    • 假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?

      我们可以建立一个组合索引,即商品ID、名称、价格作为一个组合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。所以,使用覆盖索引的好处很明显,即不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。

  • 联合索引

    • 联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
    • 建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
    • distinct
    • 区分度就是某个字段 column 不同值的个数除以表的总行数,比如性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 uuid 这类字段就比较适合做索引或排在联合索引列的靠前的位置。

小结

summary

参考资料

https://mp.weixin.qq.com/s/xaJg28qXCAC4XR2mTm4xUg

This post is licensed under CC BY 4.0 by the author.