文章目录
- [1 使用系统表](#1 使用系统表)
-
- [1.1 引言](#1.1 引言)
- [1.2 综合性能诊断](#1.2 综合性能诊断)
-
- [1.2.1 找出最耗时的 TOP 10 查询](#1.2.1 找出最耗时的 TOP 10 查询)
- [1.3 索引方面](#1.3 索引方面)
-
- [1.3.1 抓出没走索引的 SQL](#1.3.1 抓出没走索引的 SQL)
- [1.3.2 找出没人用的冗余索引](#1.3.2 找出没人用的冗余索引)
- [1.3.3 引起文件排序filesort的查询](#1.3.3 引起文件排序filesort的查询)
- [1.4 磁盘IO方面](#1.4 磁盘IO方面)
-
- [1.4.1 创建磁盘临时表的查询](#1.4.1 创建磁盘临时表的查询)
- [1.4.2 高碎片率表](#1.4.2 高碎片率表)
- [1.4.3 Buffer Pool 命中率够不够](#1.4.3 Buffer Pool 命中率够不够)
- [1.5 锁与连接方面](#1.5 锁与连接方面)
-
- [1.5.1 当前的锁等待](#1.5.1 当前的锁等待)
- [1.5.2 连接池使用情况](#1.5.2 连接池使用情况)
- [1.5.3 JOIN 效率的粗暴判断](#1.5.3 JOIN 效率的粗暴判断)
- [2 总结建议](#2 总结建议)
1 使用系统表
1.1 引言
DBA 不在身边的时候,线上 MySQL 突然慢了,第一反应是啥?
见过太多人上来就 EXPLAIN 一条业务 SQL,然后盯着 type 和 rows 看半天看不出个所以然。问题是你根本不知道"慢"到底发生在哪------是某个具体 SQL 烂了,还是锁卡住了,还是内存不够在疯狂刷盘,还是有人把连接池打满了。
排查性能问题的正确姿势不是从一条 SQL 开始,是先拿一组体检 SQL把整台数据库扫一遍,找到真正的病灶,再去局部优化。
今天把我自己排障时常用的 10 条 SQL 整理出来。每条都配了:干什么用的、阈值怎么定、实际怎么看。不是那种"收藏等于掌握"的清单,而是每条你都能直接抄到生产环境跑。
前提:MySQL 5.7+ 开了 performance_schema,8.0+ 开了 sys schema。现在这俩基本都默认开着,不用操心。
1.2 综合性能诊断
1.2.1 找出最耗时的 TOP 10 查询
这是每次上手的第一条。直接问 performance_schema 要历史累计最慢的 10 条 SQL:
sql
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR AS exec_count,
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
ROUND(MAX_TIMER_WAIT / 1000000000, 2) AS max_ms,
ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
看哪个字段: 我通常先按 total_sec 排------代表这条 SQL 累计消耗了多少秒 CPU。累计时间高的比单次慢的更要命,因为前者可能是执行了一百万次的 10ms,后者可能只是偶尔一次的 5s。
踩坑提醒:DIGEST_TEXT 是参数归一化后的模板,比如 WHERE id = ?,不会把每个具体参数单独列一行。所以看到的数量会比原始日志少得多------这是 feature 不是 bug。
1.3 索引方面
1.3.1 抓出没走索引的 SQL
慢查询里最常见的元凶就一个:全表扫描。开这个开关先:
sql
SET GLOBAL log_queries_not_using_indexes = ON;
然后从 slow_log(或者 events_statements_summary_by_digest)里看 NO_INDEX_USED_COUNT:
sql
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
SUM_NO_INDEX_USED AS no_index_count,
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_NO_INDEX_USED > 0
ORDER BY SUM_NO_INDEX_USED DESC
LIMIT 20;
阈值:no_index_count 只要 > 0 就值得看一眼。不是说没走索引就一定有问题(小表全扫可能比走索引还快),但这是个强信号。
1.3.2 找出没人用的冗余索引
索引不是越多越好。每个索引都会拖慢写入,还占磁盘和内存。线上跑了几年的库往往积累一堆"当年某个同事加上去再也没人看过"的索引。
没人用的:SELECT * FROM sys.schema_unused_indexes;
重复的(前缀相同):SELECT * FROM sys.schema_redundant_indexes;
踩坑提醒:schema_unused_indexes 的没用过是自上次 MySQL 启动以来的统计。如果刚重启过,什么都没跑,它会把所有索引都列出来。至少让库稳定跑一周再看这张表,否则会误删。
1.3.3 引起文件排序filesort的查询
排序这个操作在内存里做一般没事,一旦走磁盘 filesort 性能断崖式下跌:
sql
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
SUM_SORT_ROWS AS total_sorted,
SUM_SORT_MERGE_PASSES AS merge_passes
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_SORT_ROWS > 0
ORDER BY SUM_SORT_ROWS DESC
LIMIT 10;
阈值: 单条 SQL 的 total_sorted 超过 10 万就该警惕,超过 100 万基本就是灾难。merge_passes 大于 0 意味着真的用了磁盘归并排序。
修复方向:ORDER BY 的列加索引,或者把排序改成扫索引(ORDER BY 的列和索引顺序一致时 MySQL 就不需要 filesort 了)。
1.4 磁盘IO方面
1.4.1 创建磁盘临时表的查询
临时表本身不可怕,磁盘临时表才是性能杀手。内存装不下就会落盘,一落盘 IO 就爆。
sql
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
SUM_CREATED_TMP_DISK_TABLES AS disk_tmp,
SUM_CREATED_TMP_TABLES AS mem_tmp
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_CREATED_TMP_DISK_TABLES > 0
ORDER BY SUM_CREATED_TMP_DISK_TABLES DESC
LIMIT 10;
阈值:disk_tmp 只要大于 0 就应该看。对应的常见病因是:GROUP BY / ORDER BY 没走索引、UNION 去重、长字段被塞进了中间结果。
修复方向: 优先给 GROUP BY / ORDER BY 的列加复合索引;改不动 SQL 的话就调大 tmp_table_size 和 max_heap_table_size,让它尽量留在内存里。
1.4.2 高碎片率表
InnoDB 的表用久了会有空洞(删除/更新留下的),占空间还拖慢扫描。看碎片率:
sql
SELECT
TABLE_SCHEMA,
TABLE_NAME,
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,
ROUND(DATA_FREE / 1024 / 1024, 2) AS free_mb,
ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) * 100, 2) AS frag_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql', 'sys', 'performance_schema', 'information_schema')
AND DATA_LENGTH > 0
ORDER BY frag_pct DESC
LIMIT 20;
阈值:frag_pct 超过 20% 可以考虑 OPTIMIZE TABLE。
注意:OPTIMIZE TABLE 在 InnoDB 下会重建整张表,期间锁表(虽然 5.6+ 号称支持 online DDL,但实际场景下还是会短暂阻塞)。大表千万别白天直接跑,用 pt-online-schema-change 或业务低峰期再操作。
1.4.3 Buffer Pool 命中率够不够
InnoDB 所有读写先进 Buffer Pool,命中率低说明内存不够,数据库在疯狂刷盘:
sql
SELECT
ROUND(
(1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads')
/
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')
)) * 100, 2
) AS hit_rate_pct;
阈值: 健康值 ≥ 99%。低于 95% 说明 innodb_buffer_pool_size 配得太小了,考虑加内存或者上调这个参数(通常是物理内存的 50%--70%)。
注意: 这个计算是从启动到现在的全局累计值,刚启动不久的库命中率本来就低,看趋势比看单次快照更有意义。
1.5 锁与连接方面
1.5.1 当前的锁等待
线上偶尔会碰到SQL 不慢但就是卡住的情况,十有八九是锁等待。MySQL 8.0 用这个视图,清爽得一塌糊涂:
SELECT * FROM sys.innodb_lock_waits
输出会告诉你:等的是谁、谁在阻塞他、两边各自在执行什么 SQL、等了多少秒。
查询结果如下:
sql
*************************** 1. row ***************************
wait_started: 2024-01-01 12:00:00
waiting_pid: 123
blocking_pid: 456
waiting_query: UPDATE table SET ... WHERE ...
waiting_lock: 0x...
blocking_lock: 0x...
...
想直接杀掉阻塞方:
sql
SELECT CONCAT('KILL ', blocking_pid, ';') AS kill_cmd
FROM sys.innodb_lock_waits;
把结果复制出来执行就行。别手滑把被阻塞的那方 kill 了,影响会更大。
MySQL 5.7 没有 sys.innodb_lock_waits,用这个代替:
sql
SELECT
r.trx_id AS waiting_trx,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
1.5.2 连接池使用情况
线上事故里有一类特别招人恨:应用连接池没回收干净,MySQL 连接数被打满,新请求全部报错。
sql
SHOW STATUS LIKE 'Threads_%';
SHOW STATUS LIKE 'Max_used_connections';
SHOW VARIABLES LIKE 'max_connections';
重点看:
Threads_connected:当前连接数Max_used_connections:历史最高水位max_connections:上限
阈值:Max_used_connections / max_connections 超过 80% 就要警觉。两种原因:要么是应用突发流量要扩容,要么是连接泄漏------后者是 bug,扩容治标不治本。
怎么区分: 看 Threads_connected 趋势。如果一直单调上涨从不回落,八成是泄漏。
1.5.3 JOIN 效率的粗暴判断
最后这条很多人不知道------performance_schema 里有个每条 SQL 平均扫描了多少行、返回了多少行的统计:
sql
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
SUM_ROWS_EXAMINED AS examined,
SUM_ROWS_SENT AS sent,
ROUND(SUM_ROWS_EXAMINED / GREATEST(SUM_ROWS_SENT, 1), 2) AS scan_ratio
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_ROWS_EXAMINED > 10000
ORDER BY scan_ratio DESC
LIMIT 10;
scan_ratio 就是扫了多少行才筛出一行结果。
阈值:
< 10:健康10--100:可优化> 100:索引有问题> 1000:赶紧改,要么驱动表错了,要么关联字段没索引
2 总结建议
总结建议:
- 写个 shell 脚本把这 10 条 SQL 串起来,每天凌晨跑一遍,结果发到告警群
- 线上出性能问题时,先跑一遍这个脚本,用输出数据倒推是哪个环节出了问题
performance_schema的统计是累计值,排查之前先RUNCATE TABLE performance_schema.events_statements_summary_by_digest,这样就能只看最近这段时间的数据,不被历史污染
如果没有权限,或者不想冒险直接操作,可以使用MySQL官方提供的sys Schema存储过程,这是更安全、便捷的做法:
sql
-- 会截断 performance_schema 下所有的 summary 和 history 表
CALL sys.ps_truncate_all_tables(FALSE);
- 另外:
performance_schema本身会占 约 1% 的 CPU 和几十到几百 MB 内存。资源紧张的从库可以考虑按需开关,但生产主库强烈建议一直开着。