explain等分析工具进行MySQL慢查询分析优化

一、explain分析工具

explain是mysql的执行计划查看工具,用于分析一个查询语句的性能。

其三种格式如下:

explain select ...
explain extended select ...   # 该命令将执行计划反编译成select语句,运行 show warnings 可以得到被mysql优化后的查询语句
explain partitions select...    # 该命令用于分区表的explain命令

explain 命令的输出有以下字段:

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

下面详细叙述explain输出的各项内容。

1、id 

表示查询中执行select子句或操作表的顺序,id为几就代表是第几个select。

如果id相同,则执行的循序由上到下,例如:

mysql> explain select t.id,t.name,count(*) as arts_count from arts a join type t on a.tid=t.id where is_send=0 group by t.id\G;

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9
     filtered: 100.00
        Extra: Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
   partitions: NULL
         type: ref
possible_keys: tid
          key: tid
      key_len: 5
          ref: art.t.id
         rows: 978
     filtered: 10.00
        Extra: Using where
2 rows in set, 1 warning (0.00 sec)

他们的id相同,所以执行顺序是第一行中的表再是第二行中的表。

如果id不同,id大的优先级高,先被执行。

2、select_type 

表示每个查询的类型,有一下几种类型:

simple,primary,subquery,derived,union unoim result
  • SIMPLE:简单的 SELECT 查询,查询中不包含子查询或者 UNION;
  • PRIMARY:查询中若包含任何复杂的子查询,最外层查询标记为该标识;
  • SUBQUERY:在 SELECT 或 WHERE 中包含子查询,该子查询被标记为:SUBQUERY;
  • DEPENDENT SUBQUERY:在 SUBQUERY 基础上,子查询中的第一个SELECT,取决于外部的查询;
  • DERIVED:在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),MysqL会递归执行这些子查询,把结果放在临时表中;
  • UNION:UNION 中的第二个或后面的 SELECT 语句,则标记为UNION ; 若 UNION 包含在 FROM 子句的子查询中,外层 SELECT 将被标记为:DERIVED;
  • DEPENDENT UNION:UNION 中的第二个或后面的SELECT语句,取决于外面的查询;
  • UNION RESULT:UNION 的结果,UNION 语句中第二个 SELECT 开始后面所有 SELECT;

从 union 表中获取结果的select标记为union result。第二个select出现在union之后被标记为union,也就是union之后的select是Union类型,但union之前的那个select不是union而是primary,整个union语句是一个union result。

如 :

select * from a union select * from b        #这也是最常规的 union 联合查询

若union包含在from子句的子查询中,则外层select被标记为derived。

例如:

explain select id,title from arts where id<1500  union select id,title from arts where id>70000 \G;
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: arts
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 394
     filtered: 100.00
        Extra: Using index condition
*************************** 2. row ***************************
           id: 2
  select_type: UNION
        table: arts
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 2612
     filtered: 100.00
        Extra: Using index condition
*************************** 3. row ***************************
           id: NULL
  select_type: UNION RESULT
        table: <union1,2>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: Using temporary
3 rows in set, 1 warning (0.00 sec)

又例如:

explain select d1.name,(select id from t3) d2 from (select id,name from t1 where other_column="") d1 union (select name,id from t2);

得到结果如下

id  select_type     table
1   primary         <derived3>
3   derived         t1
2   subquery        t3
4   union           t2
null union result   <union1,4>

第一行:id为1,表示它是第一个select,select_type为primary表示该查询是一个外层查询,table列是<drived3>表示查询结果来自于一个衍生表。其中3代表这个该查询衍生来自第3个select,即id为3的select。

第二行:id为3,是第三个select,因为查询语句是在from之中,所以是derived。

第三行:id为2,第二个select是作为子查询。

第四行:id为4,查看第四个select,在union关键字后面,它是union类型且它是最先执行的。

第五行:id为null表示它是一个临时表,没有select,它代表的是整个语句,table列的<union1,4>表示它是对第一个和第四个select的结果的union操作。

3、type 

在表中找到所需行的方式,又叫访问方式。

有如下类型:

all/index/range/ref/eq_ref/const,system/null 
  • ALL:全表扫描,性能最差。
  • index:表示基于索引的全表扫描,先扫描索引再扫描全表数据。
  • range:表示使用索引范围查询。使用>、>=、<、<=、in等等。
  • ref:表示采用了非唯一索引,或者是唯一索引的非唯一性前缀。
  • eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一 行结果。
  • const:表最多有一个匹配行,因为只有一行,所以这一行中列的值可以被优化器视为常量。
  • system:system 类型一般用于 MyISAM 或 Memory 表,属于 const 类型的特例。

上面的这些type字段值,执行效率从上至下依次增强。效率从低到高依次为 all < index < range < index_merge < ref < eq_ref < const/system。

下面对type字段常用的类型做详细的说明:

all是最坏的情况,因为采用了全表扫描的方式。

index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。如果我们在 extra 列中看到 Using index,说明采用了索引覆盖,也就是索引可以覆盖所需的 SELECT 字段,就不需要进行回表,这样就减少了数据查找的开销。

range表示采用了索引范围扫描。比如下面这条SQL语句:

select * from tb_user where id>=1 and id<=5;

从range这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。 

ref类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀。

比如我们对tb_order表user_id字段创建索引:

CREATE INDEX user_id_index on tb_order(user_id)

然后查询user_id等于1的订单信息,EXPLAIN 执行计划如下:

EXPLAIN SELECT title,order_datetime FROM tb_order WHERE user_id=1

还有in、between and,如果搜索条件不是索引为条件,就会变成全表扫描ref非唯一性索引扫描,即使用普通索引或者唯一索引的非唯一前缀作为where条件搜索。

例如:

# 我设置了一个文章表的tid字段(分类id)为普通索引
explain select * from arts where tid=6;

+----+-------------+-------+------------+------+---------------+------+---------
+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len
| ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------
+-------+------+----------+-------+
|  1 | SIMPLE      | arts  | NULL       | ref  | tid           | tid  | 5
| const | 2189 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------
+-------+------+----------+-------+

explain select * from arts where title="新闻标题"; #此时的 type 是 all 全表扫描

explain select t.name,arts.*  from arts join type t on arts.tid=t.id;  #对文章表和分类表进行多表联查

+----+-------------+-------+------------+------+---------------+------+---------
+----------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len
| ref      | rows  | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------
+----------+-------+----------+-------+
|  1 | SIMPLE      | t     | NULL       | ALL  | PRIMARY       | NULL | NULL
| NULL     |     9 |   100.00 | NULL  |
|  1 | SIMPLE      | arts  | NULL       | ref  | tid           | tid  | 5
| art.t.id | 24064 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------
+----------+-------+----------+-------+

# 结果对分类表是全表扫描,对文章表是索引扫描
explain select * from arts where tid>6;         # 此时的type就变为range了

eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常用于主键和唯一索引扫描。

const、system:当mysql对查询进行优化并转化为一个常量的时候type就是const类型,例如使用主键或者唯一键作为where的条件查询,此时mysql会将这个查询结果转化为一个常量。

system是const的一个特例,当查询的表只有一行数据的时候,type就是system类型。

例如:

explain select * from arts where id=1300;    #type 为const

null: Mysql 在优化过程中分解语句,查询时甚至不用访问表或者索引。

例如:

explain select max(id) from arts;
explain select count(*) from arts;

4、possible_keys

指出 MysqL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。

如果该列是 NULL,则没有相关的索引。

5、key

​显示在查询中实际使用到的索引,若没有使用索引,显示为 NULL。

如:

explain select title from arts where id=750000;

如果要查询的字段就是这个索引,则 key 为该索引字段名,但possible_keys为null。

select id from arts

 查询中若使用了覆盖索引,则该索引可能出现在 key 列表,不出现在 possible_keys。

6、key_len

​表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。

key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的。

key_len的计算规则如下:

字符串类型:

  • 字符串长度跟字符集有关,常见编码长度:gbk=2、utf8=3、utf8mb4=4;
  • char(n):n*字符集长度;
  • varchar(n):n * 字符集长度 + 2字节;

数值类型

  • TINYINT:1个字节;
  • SMALLINT:2个字节;
  • MEDIUMINT:3个字节;
  • INT、FLOAT:4个字节;
  • BIGINT、DOUBLE:8个字节;

时间类型:

  • DATE:3个字节;
  • TIMESTAMP:4个字节;
  • DATETIME:8个字节;

在不损失精确性的前提下,长度越短越好。​

7、ref

多表联查时的连接匹配条件。

8、rows

是估算执行这条语句需要扫描的记录数,例如我的arts表有60多万条数据,我的tid字段有建立索引的。

explain select * from arts where tid=6;

结果 rows为325186,要扫描三十多万条。

当我删除tid索引的时候再执行:

explain select * from arts where tid=6;

rows614436,变成扫描60多万跳,变成了全表扫描。所以,查看性能 rows是很重要的一个指标,而建立索引和使用where的范围查询可以减少扫描的行数,如果用主键来查,发现rows只有1而已。

但其实,不一定扫描的行数越少就查的越快,例如我这个表有60万+的数据。

select title from arts where tid=6 and id>500000
select title from arts where id>500000 and tid=6;
select title from arts where id in (select id from arts where id>500000) and tid=6;

这前两条显示的rows都是311430,第三条显示的是203329和1,但是第一个用了40s,第二个用了16s,第三个只用了1.3秒,他们的结果是一样的。

这是因为子查询查的字段是主键id,又是根据主键范围为条件查的,所以会非常快,只花了0.1秒。而外层查询又是根据主键id和索引tid查的,所以也是飞快。

这里说一下:

select title from arts where id>500000
select id from arts where id>500000

这两句的条件都是用主键进行范围查询,扫描的条数也相同,但前者花了3.5秒,后者花了0.1秒而已,只因为查询的字段后者是主键。

同样说明不一定扫描行数不一定是越少就越快。

9、Extra

其他的额外的执行计划信息,在该列展示:

  • Using index:该值表示相应的 SELECT 操作中使用了覆盖索引(Covering Index);
  • Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推;

  • Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询操作未能使用索引,Using where 的作用是提醒我们 MysqL 将用 where 子句来过滤结果集,即需要回表查询;
  • Using temporary:表示 MysqL 需要使用临时表来存储结果集,常见于排序和分组查询;
  • Using filesort:对数据使用外部排序算法,将取得的数据在内存中进行排序,这种无法利用索引完成的排序操作称为文件排序;
  • Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果;
  • Impossible where:说明 where 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果;
  • Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行;
  • No tables used:Query 语句中使用 from dual 或不含任何 from 子句;

包含不适合在其他列显示但非常重要的信息,有以下4中:

Using Index:表示相应的select查询使用了覆盖索引;覆盖索引就是包含满足查询需要的所有数据的索引

Using where:表示mysql在存储引擎收到记录后进行"后过滤",并不是说查询语句中使用了where就会有Using where:

explain select id from arts where id>500000;   # 显示了Using where 
explain select * from arts where id>500000;   # 没有显示Using where 

Using temporary:表示使用了临时表来存储结果集,常用于排序和分组查询。如果同时出现了Using temporary和Using filesort 则性能是不佳的,这种情况出现在使用非索引字段分组的情况。

explain select title from arts where id>600000 group by is_send desc;
+------+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------------------------------------+
| id   | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra                                                  |
+------+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------------------------------------+
|    1 | SIMPLE      | arts  | range | PRIMARY       | PRIMARY | 4       | NULL | 122818 | Using index condition; Using temporary; Using filesort |
+------+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------------------------------------+


explain select title from arts group by id desc;
+------+-------------+-------+------+---------------+------+---------+------+--------+----------------+
| id   | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra          |
+------+-------------+-------+------+---------------+------+---------+------+--------+----------------+
|    1 | SIMPLE      | arts  | ALL  | NULL          | NULL | NULL    | NULL | 614436 | Using filesort |
+------+-------------+-------+------+---------------+------+---------+------+--------+----------------+

Using filesort:文件排序,mysql将无法利用到索引的排序操作成为文件排序,所以出现Using filesort 比不出现Using filesort的性能差。

select sql_no_cache title from arts where id>500000 order by create_time desc;      # Using index condition; Using filesort 用了3秒多
explain select sql_no_cache title from arts where id>700000 order by id desc;       # Using where; 用了1秒多

所以尽可能使用索引排序。

Explain不会告诉你关于触发器,存储过程的信息或者用户自定义函数对查询的性能影响情况。

Explain不会考虑缓存因素,并且Explain不会显示mysql在查询中所做的优化,部分统计信息是估算的,不准确。

如果有子查询或者使用了临时表的视图,使用explain的开销会很大。

总结:

EXPLAIN 命令输出字段:

  • id:select查询的序列号,表示查询中执行select子句或操作表的顺序;
  • select_type:表示 SELECT 的类型;
  • table:输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称;
  • type:表示表的连接类型;
  • possible_keys:表示查询时,可能使用的索引;
  • key:表示实际使用的索引;
  • key_len:索引字段的长度;
  • ref:列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值;
  • rows:扫描出的行数,表示 MysqL 根据表统计信息及索引选用情况,估算的找到所需的记录扫描的行数;
  • filtered:按表条件过滤的行百分比;
  • extra:执行情况的说明和描述;

MySQL执行计划的局限:

  • 只是计划,不是执行 sql 语句,可以随着底层优化器输入的更改而更改;
  • EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况;
  • EXPLAIN 不考虑各种 Cache;
  • EXPLAIN 不能显示 MysqL 在执行查询时的动态,因为执行计划在执行查询之前生成;
  • EXPALIN 部分统计信息是估算的,并非精确值;
  • EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划;
  • EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 sql 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 sql 语句实际的执行计划不同;

在使用EXPLAIN分析慢SQL的时候,我们主要关注type字段,最好可以使用到 range 这一级别及以上的 type 访问方式,如果只使用到了 all 或者 index 这一级别的访问方式,我们可以从 SQL 语句和索引设计的角度上进行改进。

其次关注rows字段,绝大部分rows小的语句执行一定很快,所以优化语句基本上都是在优化rows。

二、explain分析SQL语句案例

1、Type 访问类型

有8类,性能从好到坏依次是:

System > const > eq_ref > ref > range > index >all

要保证查询至少达到range级别,最好能达到ref级别。

System:表只有一条记录(实际中基本不存在这个情况);

Const:当条件查询是对主键或者唯一键进行精确查询(=);例如 select * from  t1 where id=1,mysql会将这条记录作为常量;

Eq_ref:唯一性索引扫描。当使用唯一键为条件查询时就是eq_ref;

例如:

select * from user where userName = ‘abc’

Ref:相对应eq_ref,当使用普通索引作为条件进行精确查询时就是ref, 普通索引可以有重复的字段值。

例如:

select * from employee where name = ‘zbp’ 

这条语句不是eq_ref而是ref,因为name不是唯一索引而是个普通索引,名字允许重名。

Eq_ref:只会返回一条记录,而ref可以返回多条记录。

Range:对添加了索引的字段进行范围查找。

如 in/bettween/</>。

Index:index表示使用了覆盖索引。Index和all相同点是都会对全部的叶子节点进行遍历。区别是index遍历的是不含行数据的叶子节点(只遍历索引值),all是遍历含有行数据的叶子节点,也就是说index遍历的次数和all一样,但是每次遍历(读取)的数据量比all小很多。所以index会比All快很多很多很多很多。

例如:

select id from t1;

All:全表扫描。

例如:

select * from t1;

2、执行select子句或操作表的顺序

3种情况:

1. id相同执行顺序由上到下

比如:

看第一列和第三列:Id都是1,所以执行的顺序是先加载t1 , 然后 t3,最后t2。

2. Id不同:如果是子查询,id的序号会递增,id值也大优先级越高,越先被执行

比如: 

看第一列和第三列:先执行查t3表的子查询,再执行t1子查询,最后查t2查询。

3. Id相同不同都存在

比如:

看第一列和第三列:首先会执行id大的sql,然后id相同的则从上到下顺序执行。

derived是衍生表的意思,也就是mysql把 select t3.id from t3 where xxx 这句的执行结果处理成了一个虚拟的表,这种把一个查询的结果临时存为一个表就是衍生表。

derived2中的2是id,表示这张衍生表是id为2的语句生成的衍生表,也就是表s1。

所以上面的执行顺序是:

3、显示哪些列或者常量被作为被检索值去进行查找

例如:

只看ref字段这里表示,t2的col1以及常量 ‘ac’ 被作为要查找的值进行检索。 

4、Using filesort 使用了文件排序

mysql中无法利用索引完成的排序,又叫做“文件排序”。出现这种情况很危险,性能不好。

文件排序不是在文件中进行排序而是在内存中进行排序,你用手指头想想怎么可能在文件中进行排序。

比如:联合索引 index(col1, col2, col3):

这条语句的where使用到了索引,但是order by排序的时候没有用到索引的排序,所以用了文件排序。意味着mysql要重新对结果集在内存中(sort_buffer 排序缓冲区)排序。 

5、Using temporary 使用临时表保存中间结果

当mysql对查询的结果排序或分组时由于内存不足以存放要排序的内容,可能会内部创建临时表。出现了这个比using filesort 还惨,因为内部创建临时表意味着会产生额外的IO。

比如:

从 Using temporary 和 Using filesort 这个例子说明,即使where用到了索引,但如果排序没用到索引,性能也不好。

6、Using index 使用了覆盖索引

出现这个,性能会很好。

如果同时出现using where ,表明索引用来执行where条件查找。

如果没有出现using where,表明select查的是索引列,但是没有执行where查找。

例如:

这是同时出现 using index 和 using where。 

只出现了 using index。

三、对SQL执行慢进行分析优化

SQL执行慢会有以下两种情况。

偶尔慢:

  • DB 在刷新脏页;
  • redo log 写满了;
  • 内存不够用,要从 LRU 链表中淘汰;
  • MySQL认为系统空闲的时候;
  • MySQL关闭时;

一直慢的原因:

  • 索引没有设计好、sql 语句没写好、MySQL选错了索引。

MySQL慢查询优化步骤:

  1. 开启MysqL慢查询日志,通过慢查询日志定位到执行较慢的sql语句。
  2. 利用explain关键字可以模拟优化器执行SQL查询语句,来分析SQL查询语句,看是否使用到了索引,以及具体的数据表访问方式。
  3. 使用SHOW PROFILE 进一步分析SQL的每一步执行时间以及CPU、IO等资源使用情况。
  4. 通过查询的结果进行优化。

优化方式有以下:

  1. 首先分析语句,看看是否包含了额外的数据,可能是查询了多余的行并抛弃掉了,也可能是加了结果中不需要的列,要对sql语句进行分析和重写。
  2. 分析优化器中索引的使用情况,要修改语句使得更可能的命中索引。比如使用组合索引的时候符合最左前缀匹配原则。not in,not like都不会走索引,可以优化为in。
  3. 如果对语句的优化已经无法执行,可以考虑表中的数据是否太大,如果是的话可以横向和纵向的切表。

1、慢查询日志定位慢SQL

慢查询日志是MySQL提供的一种日志记录,它用来记录所有执行时间超过long_query_time参数值的SQL。long_query_time的默认值为10秒,默认情况下执行超过10s的SQL语句,会被记录到慢查询日志中。

如果开启了mysql的慢日志,那么该日志会记录下所有mysql认为效率低的sql语句,我们可以通过查看慢日志获取这些语句并进行优化。

MySQL的慢查询日志比较粗略,主要是基于以下3项基本的信息:

  • Query_time:查询耗时。 
  • Rows_examined:检查了多少条记录。 
  • Rows_sent:返回了多少行记录(结果集)。 以上3个值可以大致衡量一条查询的成本。 如果检查了大量记录,而只返回很小的结果集,则往往意味着查询质量不佳。

其他信息包括如下几点:

  • Time:执行SQL的开始时间。 
  • Lock_time:等待tablelock的时间,注意InnoDB的行锁等待是不会反应在这里的。 
  • User@Host:执行查询的用户和客户端IP。

1. 开启慢查询日志

在使用前,我们需要先看下慢查询是否已经开启,使用下面这条命令查看是否开启。

show variables like '%slow_query_log%';

slow_query_log=OFF,表示未开启。 slow_query_log=ON表示慢查询已经开启,如果未开启,通过以下命令开启:

set global slow_query_log='ON';

开启后,使用show variables like '%slow_query_log%'再来看下是否开启。

slow_query_log_file是慢查询日志文件目录。

Mysql支持文件和数据库表两种日志存储方式,默认将日志存储在文件中,使用下面这条命令查看存储方式:

show variables like '%log_output%';

可以修改为存储到数据库表,这样日志信息就会被写入到mysql.slow_log表中。

set global log_output='TABLE';

MySQL数据库同时支持两种日志存储方式,配置的时候以逗号隔开即可。

set global log_output='TABLE,FILE';

日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件。 

2. 设置慢查询的时间阈值

long_query_time的默认值为10秒,通过以下命令可以查看慢查询的时间阈值。

show variables like '%long_query_time%';

如果我们想修改long_query_time参数值,通过以下命令即可,假如我们把long_query_time值设置为1。

set global long_query_time = 1;

注意:修改完再次执行show variables like ‘%long_query_time%’,发现还是默认的10S,是不是没有修改成功呢?

其实已经修改成功了,此时只需要关闭当前连接,再重新连接就可以了。

使用set global slow_query_log=1开启了慢查询日志只对当前数据库生效,如果MySQL重启后则会失效。

如果要永久生效,需要修改my.cnf配置文件,配置文件如下:

[mysqld] 
slow_query_log = 1   # 开启慢查询
slow_query_log_file = /data/mysql/logs/slow.log   # 慢查询日期路径
long_query_time = 1  # 慢查询时间阈值
log_timestamps = SYSTEM
log_output = FILE

3. 慢查询日志内容

如果自己mysql上没有慢查询,可以通过sleep(N)函数来模拟慢查询操作,比如下面这条模拟慢查询的语句。

SELECT *, sleep(3) FROM tb_user WHERE id<4

注意:sleep(N)函数表示,每返回一行数据经过WHERE条件判断后,都会触发Sleep函数,比如上面的SQL语句,返回3条数据,每条阻塞3秒,查询时间总共是9秒。

有了慢查询日志,可以使用以下命令获取有多少条日志。

show global status like '%Slow_queries%';

可以看到现在有两条慢查询,接下来我们来看下慢查询日志具体记录哪些内容。

# Time: 2022-01-22T12:56:00.070149Z
# User@Host: root[root] @ localhost []  Id:     8
# Query_time: 4.006927  Lock_time: 0.000074 Rows_sent: 4  Rows_examined: 4
SET timestamp=1642856160;
SELECT *, sleep(1) FROM tb_user WHERE id<5;

第一行,记录的是该条 SQL 语句执行的时间。

第二行,记录的是执行该SQL语句的用户和 IP 以及链接 id。

第三行的几个字段解释如下:

  • Query_time: duration 语句执行时间,以秒为单位。
  • Lock_time: duration 获取锁的时间(以秒为单位)。
  • Rows_sent: 发送给 Client 端的行数。
  • Rows_examined: 服务器层检查的行数(不计算存储引擎内部的任何处理)

第四行,记录是此SQL语句执行时候的时间戳。

第五行,就是具体的慢SQL,也是我们需要优化的SQL。

2、分析慢查询日志

可以使用mysqldumpslow命令获得慢查询日志摘要来处理慢查询日志,或者使用更好的第三方工具pt-query-digest。

慢查询日志里的慢查询不一定就是不良SQL,还可能是受其他的查询影响,或者受系统资源限制所导致的慢查询。 比如会话被阻塞了,实际上是一个行锁等待50s超时,也会被记录到了慢查询日志里。

1. mysqldumpslow 日志分析工具

MySQL 提供了 mysqldumpslow 工具统计慢查询日志。mysqldumpslow 命令的参数如下:

  • -s:采用 order 排序的方式,排序方式可以有以下几种。分别是 c(访问次数)、t(查询时间)、l(锁定时间)、r(返回记录)、ac(平均查询次数)、al(平均锁定时间)、ar(平均返回记录数)和 at(平均查询时间)。其中 at 为默认排序方式。
  • -t:返回前 N 条数据 。
  • -g:后面可以是正则表达式,对大小写不敏感。

mysqldumpslow常见用法:

mysqldumpslow -t10 绝对路径    # 获取访问时长最长的10条语句,并存到指定路径中
mysqldumpslow -s c -t10 绝对路径  # 访问次数最多的10条语句
mysqldumpslow -s r -t10 绝对路径  # 访问条数最多的10条

比如我们想要按照查询时间排序,查看前3条 SQL 语句,这样写即可:

 mysqldumpslow -s r -t 3 /var/lib/mysql/a7bb95cee15a-slow.log

得到访问次数最多的5个SQL:

mysqldumpslow -s c -t 5  /var/lib/mysql/a7bb95cee15a-slow.log

得到按照时间排序的前5条里面含有左连接的查询语句:

mysqldumpslow -s t -t 10 -g “left join”  /var/lib/mysql/a7bb95cee15a-slow.log

 另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现刷屏的情况。

mysqldumpslow -s r -t 5  /var/lib/mysql/a7bb95cee15a-slow.log | more

2. pt-query-digest 日志分析工具

首先安装pt-query_digest命令。

wget www.percona.com/get/pt-query-digest chmod u+x pt-query-digest    # 下载pt-query-digest命令
chmod u+x pt-query-digest

基本命令格式:

pt-query-digest [options] [files] [dsn]

常见用法:假设慢日志路径为 /tmp/mysql-slow.log。

pt-query-digest /tmp/mysql-slow.log > slow.rtf      #直接分析慢查询
pt-query-digest --since 1800s /tmp/mysql-slow > slow.rtf   #分析半小时内的慢查询
pt-query-digest --since '2020-01-01 00:00:00' --until '2020-01-02 00:00:00' /tmp/mysql-slow > slow.rtf    #分析指定范围内时间的慢查询
pt-query-digest --limit 95%:20 /tmp/mysql-slow > slow.rtf    #显示95%的最差查询或者20个最差查询

相应参数解释:

  • Exectime:执行时间。 
  • Lock time:表锁的时间。 
  • Rows sent:返回的结果集记录数。 
  • Rowsexamine:实际扫描的记录数。 
  • Query size:应用和数据库交互的查询文本大小。 
  • Rank:所有查询日志分析完毕后,此查询的排序。 
  • Query ID:查询的标识字符串。 
  • Responsetime:总的响应时间,以及总占比。一般小于5%可以不用关注。 
  • Calls:查询被调用执行的次数。 
  • R/Call:每次执行的平均响应时间。 
  • Apdx:应用程序的性能指数得分。(Apdex响应的时间越长,得分越低。) 
  • V/M:响应时间的方差均值比(变异数对平均数比,变异系数)。可说明样本的分散程度,这个值越大,往往是越值得考 虑优化的对象。 
  • Item:查询的简单显示,包括查询的类型和所涉及的表。

当然该命令也可以用于分析mysql二进制日志文件。

注意,二进制日志不是mysql服务的日志,二进制日志一般放在数据目录,可以通过show master status查看是否开启二进制日志。

mysqlbinlog mysql-bin.012639 > /tmp/012639.log   # 将二进制文件转为文本格式
pt-query-digest --type binlog /tmp/012639.log > binlog.rtf

对于以上分析命令,同样可以加上参数筛选信息,如“--since”、“--until”。

3. show profile 进行性能分析

show profile 相比 EXPLAIN 能看到更进一步的执行解析,包括 SQL 都做了什么、所花费的时间等。

默认情况下,show profile 是关闭的,使用下面的命令查看状态。

mysql> show variables like 'profiling';

通过设置 profiling='ON’来开启 show profile: 

mysql > set profiling = 'ON';

show profile命令只是在本会话内起作用,即无法分析本会话外的语句。

开启分析功能后,所有本会话中的语句都被分析(甚至包括执行错误的语句),除了SHOW PROFILE和SHOW PROFILES两句本身。 

查看当前会话都有哪些 profiles,使用下面这条命令:

mysql > show profiles;

使用下面的命令我们可以查看上一个查询的开销: 

mysql > show profile;

可以给show profile 指定一个 for query id 来查看指定 id 的语句,比如查看Query_ID为4的SQL信息。

mysql> show profile for query 4;

还可以给输出添加新的列,取值范围可以如下:

  • ALL 显示所有性能信息;
  • BLOCK IO 显示块IO操作的次数;
  • CONTEXT SWITCHES 显示上下文切换次数,不管是主动还是被动;
  • CPU 显示用户CPU时间、系统CPU时间;
  • IPC 显示发送和接收的消息数量;
  • PAGE FAULTS 显示页错误数量;
  • SOURCE 显示源码中的函数名称与位置;
  • SWAPS 显示SWAP的次数;

比如: 

mysql> show profile cpu,block io,swaps for query 4;

通过上面的结果,我们可以弄清楚每一步骤的耗时,以及在不同部分,比如 CPU、block io 的执行时间,这样我们就可以判断出来 SQL 到底慢在哪里。 

总结:

进行MySQL慢查询分析时,首先通过慢查询日志定位执行慢的 SQL,然后通过 EXPLAIN 分析该 SQL 语句是否使用到了索引和具体的数据表访问方式是怎样的。

如果有需要最后再使用 SHOW PROFILE 进一步了解 SQL 每一步的执行时间,包括CPU 、I/O 等资源的使用情况。

四、SQL优化案例

1、案例一

有一个文章表,只有id主键,其他字段没有建立索引。

现要求查询 category_id 为1 且 comments 大于1 的情况下,按views排序的数据。

Select * article where category_id = 1 and comments > 1 order by views

Explain 分析如下:

没有使用索引,而且排序是一个文件排序。

优化1: 创建联合索引 index idx_article_ccv (category_id, comments, views) 

此时再查,结果如下:

使用了key字段表示使用了联合索引idx_article_ccv,type表示访问类型是一个range按范围查询,但是还是使用了文件排序(using filesort)。

原因是使用了范围条件(comments>1)之后的条件或排序无法使用索引,也就是说 views 字段排序没用到索引。

优化2:删掉刚刚的索引,重新创建联合索引 index idx_article_ccv (category_id, views)

这个索引只包含两个字段,不含comments字段。 

Type的类型是ref,使用到了索引,而且没有用到文件排序,而是使用了索引B+树中的排序。

mysql会找到B+树中满足category_id=1的叶子节点(包含其他列数据),从磁盘读取到内存,然后再在内存中过滤找到comments>1的数据。由于在B+树中已经排好了序所以不会在内存中对views排序。 

2、案例二

现在有一个博客表(也是一个文章表)blogs,还有一个分类表 category 。我想做一个关联查询,关联字段是category.id = blogs.tid。假设现在category.id是主键,但是blogs.tid没有建立索引。

explain select * from blogs b join category c on b.tid=c.id;

结果如下:

发现blogs表全表扫描(type为All),没有用到索引(key为null),扫描条数为185(我的博客表一共只有185条记录);而category是唯一索引扫描(type为eq_ref),使用了主键(key为primary),只扫描了1条(category表共10个分类,有10条记录)。

博客表是一个全表扫描,效率很低。

优化1:对blogs的tid建立单列索引后再查

发现现在变成 category没有使用索引,而blogs使用了索引tid,访问类型是ref。扫描的条数变为 10+23 = 33。效率明显提高。

这个例子想说明的是:

  1. .inner  join联查,mysql默认小表作为驱动表,大表作为被驱动表,让小表驱动大表。Left  join联查,mysql会让左边的表作为驱动表,右边的表作为被驱动表。Right join同理;
  2. 多表联查会全表扫描驱动表的(例子中的表category),所以在使用left join的时候尽量把小表放在左边,让小表成为驱动表,这样会对小表进行全表扫描,好过去对大表全表扫描;
  3. 多表联查中要保证关联字段建立了索引;

猜你喜欢

转载自blog.csdn.net/qq_35029061/article/details/128641568