在MySQL数据库中,当面对一个有sql性能问题的数据库时,我们可以使用show status、explain、show profile等命令进行系统分析,尽快定位问题SQL并解决问题。
一、通过show status命令了解各种SQL的执行情况
mysql> show status like 'Com_%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| Com_admin_commands | 0 |
| Com_assign_to_keycache | 0 |
| Com_alter_db | 0 |
| Com_alter_db_upgrade | 0 |
| Com_alter_event | 0 |
| Com_alter_function | 0 |
| Com_alter_procedure | 0 |
| Com_alter_server | 0 |
| Com_alter_table | 0 |
| Com_alter_tablespace | 0 |
| Com_alter_user | 0 |
| Com_analyze | 0 |
...
Com_xxx表示xxx语句执行的次数,通过观察Com_insert、Com_select、Com_update、Com_delete等参数可以了解当前数据库是以插入更新为主还是以查询为主。以及各种类型的SQL大致的执行比例是多少。
对于事务性应用,通过Com_commit、Com_rollback可以了解事务提交和回滚的情况,对于事务回滚非常频繁的数据库,意味着应用编写可能存在问题。
二、通过explain分析低效SQL的执行计划
通过慢查询日志可以定位那些执行效率较低的SQL语句。之后可以通过explain或者desc命令获取MySQL执行select语句的信息。
mysql> explain select * from demo where id>5 and id<10 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: demo
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 3
Extra: Using where
1 row in set (0.00 sec)
explain命令显示select语句的执行信息,下面对每个字段进行说明
1、select_type
表示select的类型,常见取值有:
- select_type=SIMPLE:简单表,即不使用表连接和子查询
- select_type=PRIMARY:主查询,即外层查询
- select_type=UNION:UNION中的第二个或者后面的查询
- select_type=SUBQUERY:子查询中的第一个select
2、table
输出结果集的表
3、type
表示MySQL在表中找到所需行的方式,或者叫访问类型。常见取值有:ALL、index、range、ref、eq_ref、const/system、NULL。从左到右,性能由最差到最好。
- type=ALL:全表扫描,MySQL遍历全表来找到匹配的行
- type=index:索引全扫描。MySQL遍历整个索引来查询匹配的行
- type=range:索引范围扫描。常见于<、<=、>、>=、between等操作符
- type=ref:使用非唯一索引扫描或唯一索引前缀扫描,返回匹配某个单独值的记录行
- type=eq_ref:类似ref,区别在于使用的索引是唯一索引,对于每个索引的键值,表中只有一条记录匹配。
- type=const/system:但表中最多有一个匹配行,查询起来非常迅速。常见于根据主键primary key或唯一索引unique index进行的查询。
- type=NULL:MySQL不用访问表或者索引,直接就能得到结果。比如 select 2 > 1;
- 其他
4、possible_keys
表示查询时可能使用的索引
5、key
表示实际使用的索引
6、key_len
使用到索引字段的长度
7、rows
扫描行的数量
8、Extra
执行情况的说明和描述,包含不适合在其他列中显示,但对执行计划非常重要的额外信息
使用explain extended命令加上show warnings,可以看到SQL真正被执行前优化做了哪些改写:
mysql> explain extended select sum(salary) from demo a , role b where 1=1 and a.id=b.id and a.sex='男
' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: b
type: index
possible_keys: PRIMARY
key: roleName_id
key_len: 63
ref: NULL
rows: 6
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: a
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: test.b.id
rows: 1
filtered: 100.00
Extra: Using where
2 rows in set, 1 warning (0.00 sec)
ERROR:
No query specified
mysql> show warnings \G;
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select sum(`test`.`a`.`salary`) AS `sum(salary)` from `test`.`demo` `a` join
`test`.`role` `b` where ((`test`.`a`.`id` = `test`.`b`.`id`) and (`test`.`a`.`sex` = '男'))
1 row in set (0.00 sec)
通过show warning命令的message字段可以看到优化器去除了1=1的恒等条件,在遇到复杂SQL可以通过explain extended命令加show warning命令迅速获得一个清晰易读的语句
三、通过show profile分析SQL
通过profile,我们可以更清楚地了解SQL的执行过程,及各个过程所花时间。
查看当前MySQL是否支持profile:
mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)
默认profiling是关闭的,可以通过set语句在Session级别开启profiling:
mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
在一个InnoDB表上查询总行数:
mysql> select count(*) from tb_item;
+----------+
| count(*) |
+----------+
| 3096 |
+----------+
1 row in set (0.01 sec)
通过show profiles语句查看当前查询语句的Query ID:
mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration | Query |
+----------+------------+------------------------------+
| 1 | 0.00025375 | SELECT DATABASE() |
| 2 | 0.00505650 | select count(*) from tb_item |
+----------+------------+------------------------------+
2 rows in set, 1 warning (0.00 sec)
通过show profile for query 2 看到执行过程中线程的执行状态和消耗的时间:
mysql> show profile for query 2;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000117 |
| checking permissions | 0.000012 |
| Opening tables | 0.000028 |
| init | 0.000021 |
| System lock | 0.000015 |
| optimizing | 0.000009 |
| statistics | 0.000023 |
| preparing | 0.000019 |
| executing | 0.000005 |
| Sending data | 0.004631 |
| end | 0.000017 |
| query end | 0.000013 |
| closing tables | 0.000022 |
| freeing items | 0.000099 |
| cleaning up | 0.000028 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
为了更加直观看到排序结果,可以查询information_schema.profiling表并按消耗时间做个desc排序:
mysql> select state,sum(duration) as Total_R ,
-> round(
-> 100*sum(duration)/(select sum(duration) from information_schema.profiling
-> where query_id = 2),2
-> )as Pct_R,
-> count(*)
-> from information_schema.profiling
-> where query_id=2
-> group by state
-> order by Total_R desc;
+----------------------+----------+-------+----------+
| state | Total_R | Pct_R | count(*) |
+----------------------+----------+-------+----------+
| Sending data | 0.004631 | 91.54 | 1 |
| starting | 0.000117 | 2.31 | 1 |
| freeing items | 0.000099 | 1.96 | 1 |
| cleaning up | 0.000028 | 0.55 | 1 |
| Opening tables | 0.000028 | 0.55 | 1 |
| statistics | 0.000023 | 0.45 | 1 |
| closing tables | 0.000022 | 0.43 | 1 |
| init | 0.000021 | 0.42 | 1 |
| preparing | 0.000019 | 0.38 | 1 |
| end | 0.000017 | 0.34 | 1 |
| System lock | 0.000015 | 0.30 | 1 |
| query end | 0.000013 | 0.26 | 1 |
| checking permissions | 0.000012 | 0.24 | 1 |
| optimizing | 0.000009 | 0.18 | 1 |
| executing | 0.000005 | 0.10 | 1 |
+----------------------+----------+-------+----------+
15 rows in set (0.00 sec)
可以清晰地看到时间主要消耗在Sending data这个状态上。原因是MySQL线程要在该状态做大量的磁盘读取操作。