MySQL慢SQL排查与性能调优

总结摘要
MySQL慢SQL排查与性能调优

针对MySQL慢SQL排查与性能调优相关问题,我准备了一个“一句话原理 + 一句话源码 + 一句话项目/场景”的结构化回答,体现深度同时展现实战能力。

排查方法

如何定位慢 SQL?(慢查询日志、EXPLAIN、Profile)

原理层面: 慢 SQL 定位遵循“日志捕获 -> 执行计划分析 -> 资源消耗剖析”的递进式诊断逻辑,分别解决“谁是慢查询”、“为什么慢”以及“具体慢在哪里”三个核心问题。

源码层面: 通过调整 long_query_time 阈值触发 SQL 层的日志记录器,利用 EXPLAIN 模拟优化器生成的执行计划数据结构,并通过 Profiling 采集 SQL 执行各阶段(如 Sending data、Sorting result)的线程 CPU 与上下文切换耗时。

项目/场景层面: 生产环境遇接口响应超时,先开启慢查询日志定位阈值外的嫌疑 SQL,接着使用 EXPLAIN 发现其走了全表扫描(type=ALL),最后开启 Profile 精准定位到“Sending data”阶段耗时异常,进而通过添加索引将查询时间从秒级降至毫秒级。

EXPLAIN 中的 type 字段有哪些取值?从好到差是怎么排列的?

原理层面: type 字段描述了 MySQL 找到所需数据行的“访问路径”,从最优到最差依次为:system > const > eq_ref > ref > range > index > ALL,代表了从直接定位到全表扫描的效率衰减过程。

源码层面: 在 SQL 优化器生成执行计划时,根据索引的选择性区分度计算 join_type,如 const 对应聚簇索引上的唯一查找(ha_innodb::index_read),而 ALL 对应存储引擎接口的全表扫描调用(ha_innodb::rnd_next)。

项目/场景层面: 在核心交易系统调优时,必须强制要求 SQL 语句的 type 至少达到 range 级别,坚决消灭 ALL 全表扫描,这是保障数据库在高并发下不发生 CPU 飙升和雪崩的基石。

以下是针对 EXPLAINtype 字段从最优到最差排列的具体 SQL 语句示例及深度解释:

system

SQL 示例:

1
2
-- 针对 MyISAM 引擎或内存表,表中只有一条数据
SELECT * FROM my_system_table WHERE id = 1;

解释: 表中仅有一行数据(通常是系统表),是 const 类型的特例。此时数据库无需进行复杂的索引查找,直接读取整张表即可,效率极高。

const

SQL 示例:

1
2
3
4
-- 主键查询或唯一索引查询
SELECT * FROM user WHERE id = 1001; 
-- 或
SELECT * FROM user WHERE unique_phone = '13800000000';

解释: 针对主键或唯一索引进行等值查询。由于结果最多只有一条记录,优化器在解析阶段就能将其视为常量,读取速度极快,通常是秒级响应。

eq_ref

SQL 示例:

1
2
-- 关联查询,关联字段是驱动表的主键或唯一索引
SELECT * FROM order o JOIN user u ON o.user_id = u.id;

解释: 在关联查询中,对于驱动表的每一行,被驱动表通过主键或唯一索引进行等值查找,结果必然只有一行。这是关联查询性能最好的连接方式。

ref

SQL 示例:

1
2
-- 非唯一索引的等值查询
SELECT * FROM user WHERE age = 25;  -- 假设 age 字段有普通索引

解释: 使用非唯一索引进行等值查询。返回的数据可能有多行,虽然不如 eq_ref 精准,但依然利用了索引进行定位,避免了全表扫描。

range

SQL 示例:

1
2
3
-- 范围查询
SELECT * FROM user WHERE age BETWEEN 20 AND 30;
SELECT * FROM user WHERE id > 1000;

解释: 使用索引检索给定范围的行。相比全表扫描,它限制了扫描的索引区间,常见的 >, <, >=, <=, BETWEEN, IN 等操作符都会产生 range

index

SQL 示例:

1
2
-- 全索引扫描,通常是因为查询列都在索引中,但无过滤条件
SELECT id FROM user;  -- 假设 id 是主键索引

解释: 遍历整棵索引树,比 ALL 好在索引文件通常比数据文件小,且索引是有序的。这通常发生在“覆盖索引”场景,即查询的列恰好是索引列,不需要回表查数据。

ALL

SQL 示例:

1
2
-- 全表扫描
SELECT * FROM user WHERE name = 'ZhangSan'; -- 假设 name 字段无索引

解释: 最差的查询类型,意味着数据库必须遍历整张表(从第一行到最后一行)来寻找匹配的行。在数据量大时,这是性能杀手,必须通过添加索引优化。

你遇到过哪些导致索引失效的情况?

原理层面: 索引失效的本质是查询条件破坏了 B+ 树索引的“有序性”或“最左前缀原则”,导致优化器判定“回表成本高于全表扫描”或“无法利用索引结构定位”。

源码层面: 在 SQL 优化器阶段,若索引列参与了 Item_func 类型转换函数运算,或违反了 SEL_ARG 树的左前缀匹配规则,该索引路径会被直接剪枝,强制退化为全表扫描。

项目/场景层面: 在历史数据归档项目中,曾因 WHERE LEFT(create_time, 10) = '2025-01-01' 导致亿级表索引失效,触发全表扫描引发生产告警,后改为范围查询 WHERE create_time BETWEEN ... 后性能恢复。

如何分析 SQL 执行过程中的扫描行数和返回行数?

原理层面: 分析扫描行数与返回行数旨在评估“数据过滤效率”,若两者比值过大,说明 SQL 存在严重的“低效扫描”,即大量无用数据被读取后又被丢弃。

源码层面: MySQL 5.7+ 的 sys.schema_unused_indexesEXPLAIN 中的 rows 列基于统计信息预估扫描行数,而 EXPLAIN ANALYZE(MySQL 8.0.18+)则通过实际执行迭代器,精确输出 rows(实际扫描/返回)与 filtered(过滤率)。

项目/场景层面: 在分页查询优化中,通过 EXPLAIN ANALYZE 发现扫描行数高达 10 万但最终仅返回 20 条数据,定位到是因为 LIMIT 深度翻页导致大量无效回表,最终通过“延迟关联”先查主键再关联,将扫描行数降低 90%。

调优实战

你在项目中做过哪些 SQL 优化?能举个例子吗?

原理层面: SQL 优化的核心逻辑是“减少 IO 交互次数”与“降低 CPU/Memory 计算消耗”,通过重写 SQL 改变执行计划,利用索引覆盖、延迟关联等技术规避全表扫描与大量无效回表。

源码层面: 通过 EXPLAIN 捕获执行计划,发现 SQL 因 LIMIT 偏移量过大导致内部扫描函数 scan_next 循环调用次数远超返回行数,通过改写 SQL 触发优化器选择覆盖索引扫描,仅获取主键后再进行回表,大幅缩减 row_search_mvcc 的调用层级。

项目/场景层面: 某后台管理系统“账单流水”分页查询,在翻页至第 100 万页时接口超时;通过将原 SELECT * FROM table LIMIT 1000000, 20 重构为“延迟关联”,先通过子查询利用覆盖索引定位主键 ID,再关联查询详情,查询时间从 15 秒优化至 0.5 秒。

分页查询遇到深度分页(LIMIT 100000,10)如何优化?

原理层面: 深度分页性能瓶颈在于 MySQL 必须扫描并丢弃 OFFSET 之前的全部数据,优化核心是将“全表扫描+丢弃”转变为“索引定位+精准读取”,利用覆盖索引减少无效回表。

源码层面: 优化前执行器需调用 row_search_mvcc 逐行遍历前 10 万条记录并回表;优化后通过子查询在索引树上快速定位起始 ID,直接跳过前 10 万行的无效遍历,仅对目标行进行回表操作。

项目/场景层面: 电商订单列表在查询第 1 万页时响应超时,通过 SELECT * FROM table t JOIN (SELECT id FROM table LIMIT 100000, 10) tmp ON t.id = tmp.id 进行“延迟关联”优化,利用子查询只查主键 ID 的特性走覆盖索引,将查询耗时从 12s 降至 200ms。

如何处理大表的数据归档和删除?

原理层面: 大表归档的核心在于“分而治之”,通过化整为零的策略规避长事务锁表与 Redo Log 空间耗尽风险,采用“归档后删除”模式确保数据可追溯,并利用硬链接解决文件级删除阻塞。

源码层面: 利用 pt-archiver 工具在应用层循环发起小事务,每次仅删除 LIMIT 指定行数并提交,将大删除拆解为多次微型 DML;物理删除时通过 Linux Link 机制,使 DROP TABLE 仅删除 InnoDB 元数据与硬链接,由后台线程异步清理磁盘空间。

项目/场景层面: 针对某核心业务表累积达 9 亿数据导致查询缓慢的问题,使用 pt-archiver 将 2 亿条过期数据归档至历史库,配合 --purge 参数分批清理原表数据,全程业务无感知;表结构删除阶段采用硬链接技术,避免了直接 DROP TABLE 导致的磁盘 IO 瞬时抖动。

什么情况下适合使用 force index?有什么风险?

一句话原理
当MySQL优化器因统计信息不准、多表联查或复杂查询选择了次优索引,导致扫描行数过多或产生文件排序时,使用FORCE INDEX强制指定执行路径以提升查询性能。

一句话源码
通过EXPLAIN SELECT * FROM table_name FORCE INDEX (index_name) WHERE condition;检查执行计划,确保key列指向指定索引且rows值显著降低。

一句话项目/场景
在亿级数据报表查询中,针对created_time等分桶字段强制使用索引,避免全表扫描,将查询时间从200秒优化至1秒内。

如何优化 ORDER BY 和 GROUP BY 查询?

一句话原理
通过建立联合索引遵循最左前缀原则消除排序带来的额外文件排序操作,并利用索引的有序性直接完成分组,将全表扫描转化为索引扫描。

一句话源码
通过EXPLAIN SELECT * FROM table FORCE INDEX(idx_col1_col2) WHERE col1=1 ORDER BY col2查看执行计划,确保Extra字段显示Using index而非Using filesort

一句话项目/场景
在亿级商品检索系统中,针对分类和上架时间建立联合索引,优化分页查询SQL,彻底解决了因深分页导致的高CPU和IO抖动问题。

表数据量很大时,如何统计 count(*) 的性能问题如何解决?

一句话原理
InnoDB因MVCC机制无法维护准确的行数计数器,必须遍历索引树进行全表扫描统计,大数据量下会导致严重的I/O瓶颈。

一句话源码
利用覆盖索引优化扫描范围,如SELECT COUNT(*) FROM user FORCE INDEX (secondary_index),或直接从Redis获取GET user:total_count以避免回表。

一句话项目/场景
在亿级订单统计中,通过定时任务将昨日的聚合数据预写入中间表,供运营报表实时查询,将耗时从分钟级降至毫秒级。