MySQL优化之系统表分析SQL

文章目录

  • [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;

阈值: 单条 SQLtotal_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_sizemax_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 TABLEInnoDB 下会重建整张表,期间锁表(虽然 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 内存。资源紧张的从库可以考虑按需开关,但生产主库强烈建议一直开着。
相关推荐
Fate_I_C2 小时前
实战案例:用 Kotlin 重写一个 Java Android 工具类
android·java·kotlin
Fate_I_C2 小时前
Kotlin 特有语法糖
android·开发语言·kotlin
.柒宇.2 小时前
MySQL的MGR高可用
数据库·mysql·adb
当战神遇到编程2 小时前
MySQL核心篇:增删改查(CRUD)
数据库·mysql
Fate_I_C2 小时前
Kotlin 为什么是 Android 开发的首选语言
android·开发语言·kotlin
猿小喵2 小时前
记录一次长时间未提交事务造成的慢SQL
数据库·sql·mysql
蜡台2 小时前
Centos 安装Mysql
linux·mysql·centos·yum·mysql8
黄林晴2 小时前
Android CLI 来了!终端一键建项目、控模拟器、给 Agent 喂官方规范
android