MySQL慢查询,一口从天而降的锅,字节跳动资深面试官亲述

二、Explain分析慢查询SQL

================================================================================

分析mysql慢查询日志 ,利用explain关键字可以模拟优化器执行SQL查询语句,来分析sql慢查询语句,下面我们的测试表是一张137w数据的app信息表,我们来举例分析一下;

SQL示例如下:


-- 1.185s

SELECT * from vio_basic_domain_info where app_name like '%陈哈哈%' ; 

这是一条普通的模糊查询语句,查询耗时:1.185s,查到了148条数据;

我们用Explain分析结果如下表,根据表信息可知:该SQL没有用到字段app_name上的索引,查询类型是全表扫描,扫描行数137w。


mysql> EXPLAIN SELECT * from vio_basic_domain_info where app_name like '%陈哈哈%' ;

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

| id | select_type | table                 | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

|  1 | SIMPLE      | vio_basic_domain_info | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1377809 |    11.11 | Using where |

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

1 row in set, 1 warning (0.00 sec) 

当这条SQL使用到索引时,SQL如下:查询耗时:0.156s,查到141条数据


-- 0.156s

SELECT * from vio_basic_domain_info where app_name like '陈哈哈%' ; 

Explain分析结果如下表;根据表信息可知:该SQL用到了idx_app_name索引,查询类型是索引范围查询,扫描行数141行。由于查询的列不全在索引中(select *),因此回表了一次,取了其他列的数据。


mysql> EXPLAIN SELECT * from vio_basic_domain_info where app_name like '陈哈哈%' ;

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

| id | select_type | table                 | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

|  1 | SIMPLE      | vio_basic_domain_info | NULL       | range | idx_app_name  | idx_app_name | 515     | NULL |  141 |   100.00 | Using index condition |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

1 row in set, 1 warning (0.00 sec) 

当这条SQL使用到覆盖索引时,SQL如下:查询耗时:0.091s,查到141条数据


-- 0.091s

SELECT app_name from vio_basic_domain_info where app_name like '陈哈哈%' ; 

Explain分析结果如下表;根据表信息可知:和上面的SQL一样使用到了索引,由于查询列就包含在索引列中,又省去了0.06s的回表时间。


mysql> EXPLAIN SELECT app_name from vio_basic_domain_info where app_name like '陈哈哈%' ;

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

| id | select_type | table                 | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                    |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

|  1 | SIMPLE      | vio_basic_domain_info | NULL       | range | idx_app_name  | idx_app_name | 515     | NULL |  141 |   100.00 | Using where; Using index |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

1 row in set, 1 warning (0.00 sec) 

那么是如何通过EXPLAIN解析结果分析SQL的呢?各列属性又代表着什么?一起往下看。

2-1、各列属性的简介:


  • id:SELECT的查询序列号,体现执行优先级,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行

  • select_type:表示查询的类型。

  • table:输出结果集的表,如设置了别名,也会显示

  • partitions:匹配的分区

  • type:对表的访问方式

  • possible_keys:表示查询时,可能使用的索引

  • key:表示实际使用的索引

  • key_len:索引字段的长度

  • ref:列与索引的比较

  • rows:扫描出的行数(估算的行数)

  • filtered:按表条件过滤的行百分比

  • Extra:执行情况的描述和说明

以上标星的几类是我们优化慢查询时常用到的

2-2、慢查询分析常用到的属性


1、type:

对表访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型”。

存在的类型有: ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从低到高),介绍三个咱们天天见到的:

ALL:(Full Table Scan) MySQL将遍历全表以找到匹配的行,常说的全表扫描

index: (Full Index Scan) index与ALL区别为index类型只遍历索引树

range:只检索给定范围的行,使用一个索引来选择行

2、key

key列显示了SQL实际使用索引,通常是possible_keys列中的索引之一,MySQL优化器一般会通过计算扫描行数来选择更适合的索引,如果没有选择索引,则返回NULL。当然,MySQL优化器存在选择索引错误的情况,可以通过修改SQL强制MySQL“使用或忽视某个索引”。

  • 强制使用一个索引:FORCE INDEX (index_name)、USE INDEX (index_name)

  • 强制忽略一个索引:IGNORE INDEX (index_name)

3、rows

rows是MySQL估计为了找到所需的行而要读取(扫描)的行数,可能不精确。

4、Extra

这一列显示一些额外信息,很重要。

  • Using index

查询的列被索引覆盖,并且where筛选条件是索引的是前导列,Extra中为Using index。意味着通过索引查找就能直接找到符合条件的数据,无须回表。

注:前导列一般指联合索引中的第一列或“前几列”,以及单列索引的情况;这里为了方便理解我统称为前导列。

  • Using where

说明MySQL服务器将在存储引擎检索行后再进行过滤;即没有用到索引,回表查询。

可能的原因:

  1. 查询的列未被索引覆盖;

  2. where筛选条件非索引的前导列或无法正确使用到索引;

  • Using temporary

这意味着MySQL在对查询结果排序时会使用一个临时表。

  • Using filesort

说明MySQL会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。

  • Using index condition

查询的列不全在索引中,where条件中是一个前导列的范围

  • Using where;Using index

查询的列被索引覆盖,并且where筛选条件是索引列之一,但不是索引的前导列或出现了其他影响直接使用索引的情况(如存在范围筛选条件等),Extra中为Using where; Using index,意味着无法直接通过索引查找来查询到符合条件的数据,影响并不大。

三、一些慢查询优化经验分享

============================================================================

3-1、优化LIMIT分页


在系统中需要分页的操作通常会使用limit加上偏移量的方法实现,同时加上合适的order by 子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。

一个非常令人头疼问题就是当偏移量非常大的时候,例如可能是limit 1000000,10这样的查询,这是mysql需要查询1000000条然后只返回最后10条,前面的1000000条记录都将被舍弃,这样的代价很高,会造成慢查询。

优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。

对于下面的查询:


-- 执行耗时:1.379s

SELECT * from vio_basic_domain_info LIMIT 1000000,10; 

Explain分析结果:


mysql> EXPLAIN SELECT * from vio_basic_domain_info LIMIT 1000000,10;

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+

| id | select_type | table                 | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+

|  1 | SIMPLE      | vio_basic_domain_info | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1377809 |   100.00 | NULL  |

+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+

1 row in set, 1 warning (0.00 sec) 

该语句存在的最大问题在于limit M,N中偏移量M太大,导致每次查询都要先从整个表中找到满足条件 的前M条记录,之后舍弃这M条记录并从第M+1条记录开始再依次找到N条满足条件的记录。如果表非常大,且筛选字段没有合适的索引,且M特别大那么这样的代价是非常高的。

那么如果我们下一次的查询能从前一次查询结束后标记的位置开始查找,找到满足条件的10条记录,并记下下一次查询应该开始的位置,以便于下一次查询能直接从该位置 开始,这样就不必每次查询都先从整个表中先找到满足条件的前M条记录,舍弃掉,再从M+1开始再找到10条满足条件的记录了。

处理分页慢查询的方式一般有以下几种

思路一:构造覆盖索引

通过修改SQL,使用上覆盖索引,比如我需要只查询表中的app_name、createTime等少量字段,那么我秩序在app_name、createTime字段设置联合索引,即可实现覆盖索引,无需全表扫描。适用于查询列较少的场景,查询列数过多的不推荐。

耗时:0.390s


mysql> EXPLAIN SELECT app_name,createTime from vio_basic_domain_info LIMIT 1000000,10;

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+

| id | select_type | table                 | partitions | type  | possible_keys | key          | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+

|  1 | SIMPLE      | vio_basic_domain_info | NULL       | index | NULL          | idx_app_name | 515     | NULL | 1377809 |   100.00 | Using index |

+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+

1 row in set, 1 warning (0.00 sec) 

思路二:优化offset

无法用上覆盖索引,那么重点是想办法快速过滤掉前100w条数据。我们可以利用自增主键有序的条件,先查询出第1000001条数据的id值,再往后查10行;适用于主键id自增的场景。

耗时:0.471s


SELECT * from vio_basic_domain_info where 

  id >=(SELECT id from vio_basic_domain_info ORDER BY id limit 1000000,1) limit 10; 

原理:先基于索引查询出第1000001条数据对应的主键id的值,然后直接通过该id的值直接查询该id后面的10条数据。下方EXPLAIN 分析结果中大家可以看到这条SQL的两步执行流程。


mysql> EXPLAIN SELECT * from vio_basic_domain_info where id >=(SELECT id from vio_basic_domain_info ORDER BY id limit 1000000,1) limit 10;

+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+

| id | select_type | table                 | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+

|  1 | PRIMARY     | vio_basic_domain_info | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |      10 |   100.00 | Using where |

|  2 | SUBQUERY    | vio_basic_domain_info | NULL       | index | NULL          | PRIMARY | 8       | NULL | 1000001 |   100.00 | Using index |

+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+

2 rows in set, 1 warning (0.40 sec) 

方法三:“延迟关联”

耗时:0.439s

延迟关联适用于数量级较大的表,SQL如下;


SELECT * from vio_basic_domain_info inner join (select id from vio_basic_domain_info order by id limit 1000000,10) as myNew using(id); 

这里我们利用到了覆盖索引+延迟关联查询,相当于先只查询id列,利用覆盖索引快速查到该页的10条数据id,然后再把返回的10条id拿到表中通过主键索引二次查询。(表数据增速快的情况对该方法影响较小。)


mysql> EXPLAIN SELECT * from vio_basic_domain_info inner join (select id from vio_basic_domain_info order by id limit 1000000,10) as myNew using(id);

+----+-------------+-----------------------+------------+--------+---------------+---------+---------+----------+---------+----------+-------------+

| id | select_type | table                 | partitions | type   | possible_keys | key     | key_len | ref      | rows    | filtered | Extra       |

+----+-------------+-----------------------+------------+--------+---------------+---------+---------+----------+---------+----------+-------------+

|  1 | PRIMARY     | <derived2>            | NULL       | ALL    | NULL          | NULL    | NULL    | NULL     | 1000010 |   100.00 | NULL        |

|  1 | PRIMARY     | vio_basic_domain_info | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | myNew.id |       1 |   100.00 | NULL        |

|  2 | DERIVED     | vio_basic_domain_info | NULL       | index  | NULL          | PRIMARY | 8       | NULL     | 1000010 |   100.00 | Using index |

+----+-------------+-----------------------+------------+--------+---------------+---------+---------+----------+---------+----------+-------------+

3 rows in set, 1 warning (0.00 sec) 

3-2、排查索引没起作用的情况


  1. 模糊查询尽量避免用通配符’%'开头,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE username LIKE '%陈哈哈%' 

优化方式:尽量在字段后面使用模糊查询。如下:


SELECT * FROM t WHERE username LIKE '陈哈哈%' 

如果需求是要在前面使用模糊查询,

  • 使用MySQL内置函数INSTR(str,substr) 来匹配,作用类似于java中的indexOf(),查询字符串出现的角标位置。

  • 使用FullText全文索引,用match against 检索

  • 数据量较大的情况,建议引用ElasticSearch、solr,亿级数据量检索速度秒级

  • 当表数据量较少(几千条儿那种),别整花里胡哨的,直接用like ‘%xx%’。

但不得不说,MySQL模糊匹配大字段是硬伤,毕竟保证事务的ACID特性耗费了太多性能,因此,如果实际场景中有类似业务需求,建议果断更换大数据存储引擎如ElasticSearch、Hbase等。这里和情怀无关~

  1. 尽量避免使用 not in,会导致引擎走全表扫描。建议用 not exists 代替,如下:

-- 不走索引

SELECT * FROM t WHERE name not IN ('提莫','队长');



-- 走索引

select * from t as t1 where not exists (select * from t as t2 where name IN ('提莫','队长') and t1.id = t2.id); 

  1. 尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE id = 1 OR id = 3 

优化方式:可以用union代替or。如下:


SELECT * FROM t WHERE id = 1

   UNION

SELECT * FROM t WHERE id = 3 

  1. 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE score IS NULL 

优化方式:可以给字段添加默认值0,对0值进行判断。如下:


SELECT * FROM t WHERE score = 0 

  1. 尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。

可以将表达式、函数操作移动到等号右侧。如下:


-- 全表扫描

SELECT * FROM T WHERE score/10 = 9

-- 走索引

SELECT * FROM T WHERE score = 10*9 

  1. 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。如下:

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

中高级开发必知必会:

。如下:


-- 全表扫描

SELECT * FROM T WHERE score/10 = 9

-- 走索引

SELECT * FROM T WHERE score = 10*9 

  1. 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。如下:

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

中高级开发必知必会:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值