MySQL Query Profiler 分析SQL慢查
最近的一段时间里,线上数据库出现了很多慢查询,但是通过代码去看都是走了索引的,而且通过explain分析也是走了索引,但确实是有慢查询,很奇怪,就想通过mysql-query-profiler分析SQL慢查。
MySQL 的 Query Profiler 是一个使用非常方便的 Query 诊断分析工具,通过该工具可以获取一条Query 在整个执行过程中多种资源的消耗情况,如 CPU,IO,IPC,SWAP 等,以及发生的 PAGE FAULTS,CONTEXT SWITCHE 等等,同时还能得到该 Query 执行过程中 MySQL 所调用的各个函数在源文件中的位置。
(1) Query Profiler 的具体用法
(1.1) 查看是否开启 Query Profiler
/** 查看是否开启 Query Profiler */
mysql> show variables like 'profiling' ;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | OFF |
+---------------+-------+
1 row in set (0.03 sec)
mysql>
(1.2) 设置开启Query Profiler功能
/** 开启 Query Profiler 功能。 */
mysql> set profiling = 1 ;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql>
(1.3) 获取系统中保存的所有 Query Profiler 概要信息
/** 获取系统中保存的所有 Query 的 profile 概要信息 */
mysql> show profiles;
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 0.00138750 | show variables like 'profiling' |
| 2 | 0.19619525 | SELECT * FROM xxxx_biz_db_xxx_base_xxxxxxxx LIMIT 10 OFFSET 0 |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
mysql>
(1.4) 针对单个 Query 获取 profile 信息
/** 针对单个 Query 获取详细的 profile 信息。 */
mysql> show profile for query 2 ;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000092 |
| checking permissions | 0.000015 |
| Opening tables | 0.000023 |
| init | 0.000060 |
| System lock | 0.000018 |
| optimizing | 0.000022 |
| statistics | 0.000729 |
| preparing | 0.000037 |
| Sorting result | 0.000020 |
| executing | 0.000014 |
| Sending data | 0.000030 |
| Creating sort index | 0.194956 |
| end | 0.000049 |
| query end | 0.000017 |
| closing tables | 0.000017 |
| freeing items | 0.000034 |
| logging slow query | 0.000040 |
| cleaning up | 0.000025 |
+----------------------+----------+
18 rows in set, 1 warning (0.00 sec)
mysql>
show profile cpu,block io for query query_id;
通过Status一列,可以看到整条SQL的运行过程
1. starting //开始
2. checking permissions //检查权限
3. Opening tables //打开数据表
4. init //初始化
5. System lock //锁机制
6. optimizing //优化器
7. statistics //分析语法树
8. prepareing //预准备
9. Sorting result
10. executing //引擎执行开始
11. Sending data
12. Creating sort index
13. end //引擎执行结束
14. query end //查询结束
15. closing tables //释放数据表
16. freeing items //释放内存
17. logging slow query
18. cleaning up //彻底清理
(1.6) 针对单个Query获取详细profile信息
/** 针对单个 Query 获取详细的 profile 信息。 */
show profile cpu, block io, memory, swaps,context switches, source for query 2;
all // 显示所有的开销信息
block io //显示块IO相关开销
context switches // 上下文切换相关开销
cpu // 显示cpu相关开销
ipc // 显示发送和接收相关开销信息
memory // 显示内存相关开销
page faults // 显示页面错误相关开销
source // 显示和Source_function, Source_file, Source_line相关的开销
swaps // 显示交换次数相关开销
(2) Query Profiler实战-order by排序导致的SQL慢查
本次问题是外卖业务的评论服务遇到了sql慢查
评论表里有订单索引、店铺索引、骑手索引,所以无论是用户、商家还是骑手查询评论,肯定是走索引的。通过explain确认一下
(2.1) 首先通过explain查看sql语句分析
(2.1.1) sql语句explain-优化前
mysql> explain
-> SELECT *
-> FROM xxxx_biz_db_xxx_base_xxxxxxxx
-> WHERE shop_city_id = xxxxxxxx
-> AND rider_id = 5764611828814449467
-> AND c_d_score = 10
-> AND c_d_has_content = 1
-> AND evaluate_role = 1
-> AND is_deleted = 0
-> ORDER BY c_evaluate_timestamp DESC
-> LIMIT 10 OFFSET 0 ;
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | xxxx_biz_db_xxx_base_xxxxxxxx | NULL | ref | idx_rider_id | idx_rider_id | 8 | const | 2174 | 0.00 | Using index condition; Using where; Using filesort |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql>
可以看到走了骑手索引idx_rider_id,但是备注里提升用了文件排序 Using filesort,
一看就知道是order by
排序导致Using filesort
如果MySQL内存够用优先在内存里排序,内存不够用了才会使用文件排序,但是耗时就高了。
(2.1.2) sql语句explain-优化后
把sql语句优化了一下,把 ORDER BY c_evaluate_timestamp DESC
改成 ORDER BY id DESC
试一下
mysql> explain
-> SELECT *
-> FROM xxxx_biz_db_xxx_base_xxxxxxxx
-> WHERE shop_city_id = xxxxxxxx
-> AND rider_id = 5764611828814449467
-> AND c_d_score = 10
-> AND c_d_has_content = 1
-> AND evaluate_role = 1
-> AND is_deleted = 0
-> ORDER BY id DESC
-> LIMIT 10 OFFSET 0 ;
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | xxxx_biz_db_xxx_base_xxxxxxxx | NULL | ref | idx_rider_id | idx_rider_id | 8 | const | 2174 | 0.00 | Using where |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
mysql>
通过explain发现,没有文件排序Using filesort
了
(2.2.4) 获取系统中保存的所有 Query Profiler 概要信息
mysql> show profiles;
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 0.00138750 | show variables like 'profiling' |
| 2 | 0.19619525 | SELECT *
FROM xxxx_biz_db_xxx_base_xxxxxxxx
WHERE shop_city_id = 'xxxxxxxx'
AND rider_id = '5764611828814449467'
AND c_d_score = '10'
AND c_d_has_content = '1'
AND evaluate_role = 1
AND is_deleted = 0
ORDER BY c_evaluate_timestamp DESC
LIMIT 10 OFFSET 0 |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
mysql>
可以看到有2条查询信息,第二条是我们想要查看的
(2.2.5) 获取sql的详细profile信息
mysql> show profile for query 2 ;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000092 |
| checking permissions | 0.000015 |
| Opening tables | 0.000023 |
| init | 0.000060 |
| System lock | 0.000018 |
| optimizing | 0.000022 |
| statistics | 0.000729 |
| preparing | 0.000037 |
| Sorting result | 0.000020 |
| executing | 0.000014 |
| Sending data | 0.000030 |
| Creating sort index | 0.194956 |
| end | 0.000049 |
| query end | 0.000017 |
| closing tables | 0.000017 |
| freeing items | 0.000034 |
| logging slow query | 0.000040 |
| cleaning up | 0.000025 |
+----------------------+----------+
18 rows in set, 1 warning (0.00 sec)
mysql>
通过Query Profiler确认,Creating sort index
确实耗时,200ms的查询有194ms耗费在了排序上。
(3) Query Profiler实战-IO导致的慢查
商家端会有查询某个商家评论的需求,这次遇到的问题是商家查询近x天用户评论商家的SQL
(3.1) 通过explain确认走索引
mysql> explain
-> SELECT COUNT(*) AS count
-> FROM xxxx_biz_db_xxx_base_xxxxxxxx
-> WHERE shop_city_id = xxxxxxxx
-> AND shop_id = '5764607706930086100'
-> AND c_b_score IN ('2', '10', '100', '200', '300', '400', '500')
-> AND c_evaluate_timestamp BETWEEN '1666242000' AND '1666933199'
-> AND evaluate_role = 1
-> AND is_deleted = 0
-> ;
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | xxxx_biz_db_xxx_base_xxxxxxxx | NULL | ref | idx_shop_id | idx_shop_id | 8 | const | 2174 | 0.00 | Using index condition; Using where; |
+----+-------------+-------------------------------+------------+------+---------------+--------------+---------+-------+------+----------+----------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql>
shop_id=5764607706930086100的店铺评价有1229条
根据shop_id索引扫描到1229条数据
然后需要根据c_b_score字段过滤
由于索引里没有,mysql回表读取数据到内存里,
然后再过滤,最后得到结果5条
(3.2) Query Profiler分析
Query Profiler处于开启状态,直接进入分析阶段
(3.2.1)获取系统中保存的所有 Query Profiler 概要信息
mysql> show profiles;
+----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 0.00070225 | show variables like 'profiling' |
| 2 | 0.13942025 | SELECT COUNT(*) AS count
FROM xxxx_biz_db_xxx_base_xxxxxxxx
WHERE shop_city_id = 'xxxxxxxx'
AND shop_id = '5764607706930086100'
AND c_b_score IN ('2', '10', '100', '200', '300', '400', '500')
AND c_evaluate_timestamp BETWEEN '1666242000' AND '1666933199'
AND evaluate_role = 1
AND is_deleted = |
+----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
mysql>
(3.2.2) 获取sql的详细profile信息
mysql> show profile for query 2 ;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000105 |
| checking permissions | 0.000015 |
| Opening tables | 0.000024 |
| init | 0.000042 |
| System lock | 0.000017 |
| optimizing | 0.000023 |
| statistics | 0.000495 |
| preparing | 0.000039 |
| executing | 0.000013 |
| Sending data | 0.138473 |
| end | 0.000045 |
| query end | 0.000016 |
| closing tables | 0.000018 |
| freeing items | 0.000032 |
| logging slow query | 0.000043 |
| cleaning up | 0.000022 |
+----------------------+----------+
16 rows in set, 1 warning (0.00 sec)
mysql>
最耗时的竟然是发送数据Sending data
,有点不太相信
(3.2.3) 获取sql的详细profile信息
再来一次
mysql> show profiles;
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 0.00204875 | show variables like 'profiling' |
| 2 | 0.13039850 | SELECT COUNT(*) AS count
FROM xxxx_biz_db_xxx_base_xxxxxxxx
WHERE shop_city_id = 'xxxxxxxx'
AND shop_id = '5764607706930086100'
AND c_b_score IN ('2', '10', '100', '200', '300', '400', '500')
AND c_evaluate_timestamp BETWEEN '1666242000' AND '1666933199'
AND evaluate_role = 1
AND is_deleted = 0 |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
mysql>
(3.2.4) 获取sql的profile信息
mysql> show profile for query 2 ;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000145 |
| checking permissions | 0.000020 |
| Opening tables | 0.000030 |
| init | 0.000062 |
| System lock | 0.000021 |
| optimizing | 0.000031 |
| statistics | 0.000664 |
| preparing | 0.000041 |
| executing | 0.000016 |
| Sending data | 0.129180 |
| end | 0.000042 |
| query end | 0.000017 |
| closing tables | 0.000018 |
| freeing items | 0.000028 |
| logging slow query | 0.000054 |
| cleaning up | 0.000033 |
+----------------------+----------+
16 rows in set, 1 warning (0.00 sec)
mysql>
(3.2.5) 获取sql的详细profile信息
mysql> show profile cpu, block io for query 2;
+----------------------+----------+----------+------------+--------------+---------------+
| Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
+----------------------+----------+----------+------------+--------------+---------------+
| starting | 0.000145 | 0.000126 | 0.000000 | 0 | 0 |
| checking permissions | 0.000020 | 0.000019 | 0.000000 | 0 | 0 |
| Opening tables | 0.000030 | 0.000030 | 0.000000 | 0 | 0 |
| init | 0.000062 | 0.000062 | 0.000000 | 0 | 0 |
| System lock | 0.000021 | 0.000020 | 0.000000 | 0 | 0 |
| optimizing | 0.000031 | 0.000000 | 0.000074 | 0 | 0 |
| statistics | 0.000664 | 0.000258 | 0.000000 | 96 | 0 |
| preparing | 0.000041 | 0.000039 | 0.000000 | 0 | 0 |
| executing | 0.000016 | 0.000016 | 0.000000 | 0 | 0 |
| Sending data | 0.129180 | 0.015645 | 0.008681 | 33824 | 0 |
| end | 0.000042 | 0.000026 | 0.000000 | 0 | 0 |
| query end | 0.000017 | 0.000017 | 0.000000 | 0 | 0 |
| closing tables | 0.000018 | 0.000017 | 0.000000 | 0 | 0 |
| freeing items | 0.000028 | 0.000028 | 0.000000 | 0 | 0 |
| logging slow query | 0.000054 | 0.000054 | 0.000000 | 0 | 16 |
| cleaning up | 0.000033 | 0.000033 | 0.000000 | 0 | 0 |
+----------------------+----------+----------+------------+--------------+---------------+
16 rows in set, 1 warning (0.00 sec)
mysql>
通过Block_ops_in可以看到确实有大量数据读取
Block_ops_in和Block_ops_out表示块存储设备输入和输出的次数,即从硬盘读取和写入数据的次数。
正常情况下,只有当数据量大于内存可用量时,才会借助硬盘进行内存交换(Swap),因此产生如此大量的硬盘读取和写入。
有几个办法:
第一个办法,优化查询语句,减少数据量。
第二个办法,索引覆盖。
第三个办法,增大MySQL内存。
我们首先分析了sql语句和索引,发现sql语句只走了店铺索引,然后回表取回了一千多条数据,再根据 评价时间c_evaluate_timestamp
、评分c_b_score
、角色evaluate_role
做过滤,
而且通过代码和sql发现查询用的select *
,也就是回表的时候取的是所有的字段,通过show tabl status
发现该表的AVG_row_length
是203字节,计算一下 203*1000字节 回表会取200字节左右数据,所以我们把select *
优化成select 必要字段
而且优化索引,把店铺索引改为店铺评价时间索引,这样通过索引能过滤大部分的数据
(4) show profile all for query 6
mysql> show profile all for query 6 ;
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-------------------------+----------------------+-------------+
| Status | Duration | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function | Source_file | Source_line |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-------------------------+----------------------+-------------+
| starting | 0.000117 | 0.000075 | 0.000042 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 0 | NULL | NULL | NULL |
| Executing hook on transaction | 0.000028 | 0.000006 | 0.000022 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 0 | launch_hook_trans_begin | rpl_handler.cc | 1409 |
| starting | 0.000016 | 0.000006 | 0.000009 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | launch_hook_trans_begin | rpl_handler.cc | 1411 |
| checking permissions | 0.000005 | 0.000004 | 0.000002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | check_access | sql_authorization.cc | 2160 |
| Opening tables | 0.002163 | 0.000072 | 0.000453 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 24 | 0 | open_tables | sql_base.cc | 5784 |
| init | 0.000018 | 0.000008 | 0.000010 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | execute | sql_select.cc | 559 |
| System lock | 0.000025 | 0.000010 | 0.000015 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 0 | mysql_lock_tables | lock.cc | 331 |
| optimizing | 0.000011 | 0.000009 | 0.000002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | optimize | sql_optimizer.cc | 297 |
| statistics | 0.000536 | 0.000156 | 0.000380 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 78 | 0 | optimize | sql_optimizer.cc | 624 |
| preparing | 0.000061 | 0.000044 | 0.000018 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | optimize | sql_optimizer.cc | 708 |
| executing | 0.062404 | 0.053891 | 0.007371 | 0 | 95 | 0 | 0 | 0 | 0 | 0 | 1380 | 0 | ExecuteIteratorQuery | sql_union.cc | 1196 |
| end | 0.000015 | 0.000006 | 0.000009 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | execute | sql_select.cc | 592 |
| query end | 0.000004 | 0.000002 | 0.000001 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | mysql_execute_command | sql_parse.cc | 4703 |
| waiting for handler commit | 0.000032 | 0.000012 | 0.000020 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 6 | 0 | ha_commit_trans | handler.cc | 1594 |
| closing tables | 0.000009 | 0.000007 | 0.000002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | mysql_execute_command | sql_parse.cc | 4761 |
| freeing items | 0.000048 | 0.000023 | 0.000025 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | dispatch_sql_command | sql_parse.cc | 5216 |
| logging slow query | 0.000061 | 0.000018 | 0.000044 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | log_slow_do | log.cc | 1644 |
| cleaning up | 0.000044 | 0.000016 | 0.000026 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7 | 0 | dispatch_command | sql_parse.cc | 2360 |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-------------------------+----------------------+-------------+
18 rows in set, 1 warning (0.00 sec)
mysql>
参考资料
[1] MySQL 8.0 show-profile
[2] MySQL- SQL执行计划 & 统计SQL执行每阶段的耗时
[3] MySQL分析SQL耗时瓶颈
[4] MySQL Profiling 的使用
[5] MySQL PROFILING 不仅仅是性能检测