Mysql高级篇学习总结11:定位执行慢的sql方法、分析查询语句EXPLAIN的使用
1、定位执行慢的sql
mysql的慢查询日志,用来记录在mysql中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的sql,会被记录到慢查询中。
默认情况下,mysql数据库没有开启慢查询日志,需要手动来设置这个参数。如果不是调优需要的话,一般不建议开启该参数,因为开启慢查询日志或多或少会带来一定的性能影响。
1.1 开启慢查询日志参数
1、开启slow_query_log
可以使用以下命令查看是否开启慢查询日志:
mysql> show variables like '%slow_query_log';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | OFF |
+----------------+-------+
1 row in set (0.63 sec)
可以开启该参数:
mysql> set global slow_query_log = on;
Query OK, 0 rows affected (0.59 sec)
开启该参数之后,再来查看该参数相关的变量:
可以看到,此时slow_query_log已经打开了,同时该文件对应磁盘上的位置也出来了。
mysql> show variables like '%slow_query_log%';
+---------------------+-----------------------------------+
| Variable_name | Value |
+---------------------+-----------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/koping-HP-slow.log |
+---------------------+-----------------------------------+
2 rows in set (0.00 sec)
2、修改long_query_time阈值
接下来先查看慢查询的时间阈值设置:
mysql> show variables like '%long_query_time%';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)
默认是10秒以上的才是慢查询,这里为了演示方便,将时间缩短为1秒:
全局修改:set global long_query_time=1;
会话修改:set long_query_time=1;
-- 全局修改
mysql> set global long_query_time=1;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%long_query_time%';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)
-- 会话修改
mysql> set long_query_time=1;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%long_query_time%';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+
1 row in set (0.01 sec)
1.2 查看慢查询数目
查询当前系统中有多少慢查询记录:
mysql> SHOW GLOBAL STATUS LIKE '%Slow_queries%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries | 0 |
+---------------+-------+
1 row in set (0.00 sec)
1.3 案例演示
这里借助上一篇博文(Mysql高级篇学习总结10:适合创建索引的11种情况、不适合创建索引7的种情况)在1.1中创建的student_info表来进行演示,该表中有100万条数据。
1.4 测试及分析
运行一条耗时较长的sql语句:
mysql> SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id ORDER BY create_time DESC LIMIT 100;
100 rows in set (1.92 sec)
可以看到该sql的语句超过了1秒,此时再来查询慢查询个数,此时变为了1了。
mysql> SHOW GLOBAL STATUS LIKE '%Slow_queries%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries | 1 |
+---------------+-------+
1 row in set (0.00 sec)
1.5 慢查询日志分析工具:mysqldumpslow
发下有了慢查询之后,可以通过慢查询日志分析工具进行分析了。首先看下改工具有哪些参数:
koping@koping-HP:~$ mysqldumpslow -h
Option h requires an argument
ERROR: bad option
Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]
Parse and summarize the MySQL slow query log. Options are
--verbose verbose
--debug debug
--help write this text to standard output
-v verbose
-d debug
-s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is default
al: average lock time
ar: average rows sent
at: average query time
c: count
l: lock time
r: rows sent
t: query time
-r reverse the sort order (largest last instead of first)
-t NUM just show the top n queries
-a don't abstract all numbers to N and strings to 'S'
-n NUM abstract numbers with at least n digits within names
-g PATTERN grep: only consider stmts that include this string
-h HOSTNAME hostname of db server for *-slow.log filename (can be wildcard),
default is '*', i.e. match all
-i NAME name of server instance (if using mysql.server startup script)
-l don't subtract lock time from total time
接着来查询下目前慢查询日志记录的前5条慢查询语句有哪些。可以看到,我们确实看到了刚刚测试中用到的那条慢查询语句。
mysqldumpslow -a -s t -t 5 /var/lib/mysql/koping-HP-slow.log
root@koping-HP:~# mysqldumpslow -a -s t -t 5 /var/lib/mysql/koping-HP-slow.log
Reading mysql slow query log from /var/lib/mysql/koping-HP-slow.log
Count: 1 Time=1.93s (1s) Lock=0.00s (0s) Rows=100.0 (100), root[root]@localhost
SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id ORDER BY create_time DESC LIMIT 100
Died at /usr/bin/mysqldumpslow line 167, <> chunk 1.
1.6 关闭慢查询日志
1)停止慢查询日志功能:
set global slow_query_log=off;
2、分析查询语句:EXPLAIN
2.1 概述
定位到了查询慢的sql之后,就可以使用EXPALIN或者DESCRIBE工具做针对性的分析查询语句。
2.2 基本语法
EXPALIN或DESCRIBE语句的语法形式如下:
EXPALIN SELECT select_options
或者
DESCRIBE SELECT select_options
注意:EXPALIN输出的是执行计划,并不是实际真实执行了该语句。
EXPALIN语句输出的各个列的作用如下:
2.3 测试数据准备
1)创建2张表
CREATE TABLE s1 (
id INT AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
INDEX idx_key1 (key1),
UNIQUE INDEX idx_key2 (key2),
INDEX idx_key3 (key3),
INDEX idx_key_part(key_part1, key_part2, key_part3)
) ENGINE=INNODB CHARSET=utf8;
CREATE TABLE s2 (
id INT AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
INDEX idx_key1 (key1),
UNIQUE INDEX idx_key2 (key2),
INDEX idx_key3 (key3),
INDEX idx_key_part(key_part1, key_part2, key_part3)
) ENGINE=INNODB CHARSET=utf8;
2)创建存储函数
DELIMITER //
CREATE FUNCTION rand_string1(n INT)
RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i = i + 1;
END WHILE;
RETURN return_str;
END //
DELIMITER ;
3)创建2个存储过程
DELIMITER //
CREATE PROCEDURE insert_s1 (IN min_num INT (10),IN max_num INT (10))
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO s1 VALUES(
(min_num + i),
rand_string1(6),
(min_num + 30 * i + 5),
rand_string1(6),
rand_string1(10),
rand_string1(5),
rand_string1(10),
rand_string1(10));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
DELIMITER //
CREATE PROCEDURE insert_s2 (IN min_num INT (10),IN max_num INT (10))
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO s2 VALUES(
(min_num + i),
rand_string1(6),
(min_num + 30 * i + 5),
rand_string1(6),
rand_string1(10),
rand_string1(5),
rand_string1(10),
rand_string1(10));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
4)#调用存储过程,往两张表里分别插入1万条数据
mysql> CALL insert_s1(10001,10000);
Query OK, 0 rows affected (9.64 sec)
mysql> CALL insert_s2(10001,10000);
Query OK, 0 rows affected (11.64 sec)
5)查看测试用的数据是否查看成功
mysql> SELECT COUNT(*) FROM s1;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)
mysql> SELECT COUNT(*) FROM s2;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)
2.4 EXPLAIN各列作用
2.4.1 table
查询的每一行记录都对应者一个单表
mysql> EXPLAIN SELECT * FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 9895 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 9895 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 9895 | 100.00 | Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
2 rows in set, 1 warning (0.00 sec)
2.4.2 id
在一个大的查询语句中,每个SELECT关键字都对应着一个唯一的id
比如下面有2个select查询语句,那么就有2个id。
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3='a';
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| 1 | PRIMARY | s1 | NULL | ALL | idx_key3 | NULL | NULL | NULL | 9895 | 100.00 | Using where |
| 2 | SUBQUERY | s2 | NULL | index | idx_key1 | idx_key1 | 303 | NULL | 9895 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
但是,需要注意,查询优化器可能对涉及子查询的查询语句进行重写。
比如下面的这条语句,虽然有2个select,但是查询优化器进行了优化,因此结果还是只有1个id:
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE common_field='a');
+----+-------------+-------+------------+------+---------------+----------+---------+-----------------+------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-----------------+------+----------+------------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | idx_key1 | NULL | NULL | NULL | 9895 | 100.00 | Using where |
| 1 | SIMPLE | s2 | NULL | ref | idx_key2 | idx_key2 | 5 | dbtest1.s1.key1 | 1 | 10.00 | Using index condition; Using where |
+----+-------------+-------+------------+------+---------------+----------+---------+-----------------+------+----------+------------------------------------+
2 rows in set, 2 warnings (0.00 sec)
小结id:
- id如果相同,可以认为是一组,从上往下顺序执行
- 在所有组中,id值越大,优先级越高,越先执行
- 关注点:id号每个号码,表示一趟独立的查询,一个sql的查询趟数越少越好
2.4.3 select_type
mysql为每一个select关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色。我们看一下select_type都能取哪些值:
2.4.4 partitions
代表分区表中的命中情况,非分区表,该项为NULL。一般情况下的查询语句的执行计划的partitions列的值都是NULL
2.4.5 type
执行计划的一条记录就代表着mysql对某个表的执行查询时的访问方法,其中type列就表明了这个访问方法是什么。
结果值从最好到最坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > All
sql性能优化的目标:至少要达到range级别,要求是ref级别,最好是consts级别。(阿里巴巴开发手册要求)
2.4.6 possible_keys和key
possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。
key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。
2.4.7 key_len
实际用到的索引长度(即字节数)
2.4.8 ref
当使用索引列等值查询时,与索引列进行等值匹配的对象信息。
该字段与type字段是对应匹配一起看的。
2.4.9 rows
预估的需要读取的记录树,该值越小越好。
2.4.10 filtered
某个表经过搜索条件过滤后剩余记录条数的百分比。
对于单表查询来说,该列没有什么意义。在连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动动表要执行的次数:rows * filtered
2.4.11 Extra
Extra是用来说明一些额外信息的,可以提供mysql到底是如何执行给定的查询语句的。