SQL 优化听起来像是 DBA 的专属技能,但实际工作中,大多数性能问题都源于开发者写出的 SQL 不够高效。掌握一套通用的排查和优化流程,远比背一堆零散的技巧更重要。这一节不堆砌命令,而是帮你建立从“发现问题”到“验证效果”的完整闭环。
13.1.1 优化之前的自我提问
并不是所有慢 SQL 都值得优化。真正陷入误区的是:花半天时间把一个每天只跑一次、耗时 2 秒的报表 SQL 优化到 0.5 秒,却对线上每分钟执行几万次、平均耗时 100 毫秒的查询视而不见。开始优化前,先问自己几个问题:
- 频率多高? 高并发下,哪怕 10 毫秒的差异也会被放大成巨大的系统负载。
- 影响多大? 这条 SQL 是否阻塞了核心业务流程,或导致连接池打满?
- 能否绕开? 有些查询是否可以通过缓存、异步化、产品设计上的妥协来彻底避免?
把精力放在高频、高消耗、核心链路的 SQL 上,是优化工作的第一要务。
13.1.2 第一步:精准定位慢查询
慢查询不会自己跳出来,需要主动暴露。MySQL 提供了慢查询日志,这是定位问题的起点。
1 确保慢查询日志已开启
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.1; -- 阈值设为 100 毫秒
SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未使用索引的查询
线上环境通常配置为记录超过一定阈值(如 200 毫秒)的 SQL,阈值太小会刷爆磁盘,太大则会漏掉瓶颈。
2 分析慢查询日志
原生日志格式可读性一般,通常配合工具使用:
- mysqldumpslow:MySQL 自带的日志分析命令,可以按耗时、扫描行数、出现次数排序。
- pt-query-digest:Percona Toolkit 中的明星工具,能生成详细的报告,按查询指纹聚合,一眼看出哪类 SQL 最拖后腿。
如果你使用了云 RDS,控制台一般会直接展示慢 SQL 统计,也支持下载详细日志。
3 实时抓取运行中的慢查询
有时慢查询是偶发的,等你去看日志已经晚了。可以在线抓取:
-- 查看当前正在执行且耗时较长的线程
SELECT * FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5;
通过 MySQL 8.0 的 sys.session 视图也能看到类似信息,甚至可以直接看到 SQL 执行进度(progress 列)。
13.1.3 第二步:用 EXPLAIN 看清执行计划
拿到一条慢 SQL 之后,下一个问题就是:“它到底是怎么执行的?”
EXPLAIN 是回答这个问题的核心工具。它会展示 MySQL 优化器为这条查询选择的执行路径,包括表访问顺序、使用的索引、扫描行数预估等。
EXPLAIN SELECT * FROM orders WHERE user_id = 1002 ORDER BY create_time DESC;
对于 DELETE、UPDATE、INSERT ... SELECT 等非查询语句,也可以通过 EXPLAIN 查看执行计划。MySQL 8.0 还支持 EXPLAIN ANALYZE,可以直接运行查询并返回每一步的实际耗时和行数,比单纯的预估更可靠。
解读 EXPLAIN 时,不要试图一次性记住所有字段,重点关注几个“信号灯”:
- type:连接类型/访问方法。从优到劣大致是:
const、eq_ref、ref、range、index、ALL。出现ALL(全表扫描)就要高度警惕,index(全索引扫描)通常也不够理想。 - key:实际选择的索引。如果这里是 NULL 而业务上应该有索引可用,说明索引设计或写法出了问题。
- rows:优化器预估需要检查的行数。这是相对数,用于估算工作量,但可以和实际返回行数对比,看是否出现严重偏差。
- Extra:包含重要的补充信息。
Using index:表示使用覆盖索引,不需要回表,是好信号。Using where:表示在存储引擎返回行后进行了条件过滤,如果配合rows很大,可能说明索引不精确。Using filesort:需要额外排序,如果排序列上没有索引,要注意。Using temporary:需要临时表,通常在 GROUP BY、DISTINCT 或 UNION 时出现,可能是隐患。
简单判断法:如果 EXPLAIN 显示 type 是 ALL 或 index,且 rows 特别大(几十万、上百万),这条 SQL 几乎一定快不起来。优化方向就是想办法让 key 列出现合适的索引,让 type 提升为 ref 或 range,再让 Extra 尽量出现 Using index。
13.1.4 第三步:深入诊断执行细节
如果 EXPLAIN 已经告诉你查询走了索引,但实际仍然很慢,就需要进入更细粒度的诊断。
1 SHOW PROFILE(即将弃用但依然可用)
可以分析一条 SQL 在各阶段的耗时分布(打开表、关联、排序、发送数据等)。虽然官方推荐用 Performance Schema 替代,但在开发环境快速排查仍有价值。
SET profiling = 1;
SELECT ...;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
2 OPTIMIZER_TRACE(优化器追踪)
当怀疑优化器选错了索引时,OPTIMIZER_TRACE 是最好的诊断工具。它会输出优化器决策的全过程:评估了哪些执行计划,每个计划的预估成本是多少,为什么选择其中一个而放弃另一个。
SET optimizer_trace = 'enabled=on';
SELECT * FROM t WHERE ...;
SELECT * FROM information_schema.optimizer_trace\G
输出内容是 JSON 格式,可以清晰看到“considered_execution_plans”中不同索引的成本比较。有时明明有更好的索引,优化器却选了另一个,通常是因为索引统计信息不准确。此时一条 ANALYZE TABLE 可能就解决问题了。
3 Performance Schema 和 sys 库
MySQL 8.0 下,sys 库提供了很多直观的视图,比如 sys.schema_unused_indexes 可找出未使用的冗余索引,sys.statements_with_full_table_scans 可找出全表扫描的语句。虽然无法替代 EXPLAIN,但可以帮你从全局视角发现系统性风险。
13.1.5 第四步:制定并实施优化方案
经过前三步,问题的具体原因通常已经浮出水面。优化方向无外乎以下几种:
1 索引优化(最常见、最有效)
检查是否缺失必要的索引,或者索引列的顺序不对。是否可以利用覆盖索引避免回表?联合索引是否遵循最左前缀?是否存在索引失效的情况(如隐式类型转换、对列进行函数操作)?索引相关细节会在第 12 章展开,但思路是:用最小的索引代价,让查询的 type 提升到 ref/range,Extra 中消除 filesort 和 temporary。
2 SQL 改写
有时 SQL 本身就写得很“重”。常见的优化点:
- 避免 SELECT \*:只取需要的列,既能减少数据传输,也更容易利用覆盖索引。
- 分页优化:深分页(
LIMIT 100000, 20)时要考虑基于索引的延迟关联或标记位分页,而不是让数据库扔掉前 10 万行。 - 子查询转连接:有些子查询在优化器中会被自动转换,但显式写成 JOIN 往往更可控,性能更稳定。
- 用 UNION ALL 代替 UNION:如果不需要去重,UNION ALL 不会产生临时表排序的开销。
- 避免在 WHERE 子句中对列进行运算:如
WHERE YEAR(create_time) = 2024会导致索引失效,应写成WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'。
3 架构与设计调整
如果索引和 SQL 本身已经没有明显问题,可能需要往更上层考虑:表是否可以垂直拆分,把大字段移出主表?是否可以用缓存(Redis)扛住热点读请求?读写分离是否能分担查询压力?这些属于架构范畴,但对某些极端查询,可能是唯一的解决思路。
13.1.6 第五步:验证优化效果并形成闭环
优化不是“感觉快了”就行,必须用数据说话。在相同的数据量和并发条件下,对比优化前后:
- EXPLAIN 中的 type、key、rows、Extra 变化。
- SQL 实际执行时间(可以用
SELECT BENCHMARK()或客户端计时)。 - 对系统整体负载(CPU、IO、连接数)的改善。
如果优化有效,记得把经验和修改固化下来:更新代码仓库中的 SQL 或 ORM 查询,必要时补充注释说明为什么用这个索引、为什么这样写。如果优化无效,也别怕退回到原方案,切忌为了优化而优化,以牺牲可读性或逻辑正确性为代价。
最后,查询优化是一项需要反复练习的技能,但它本质上是“观察—诊断—修复—验证”的科学流程,而非玄学。用好慢查询日志,看懂 EXPLAIN,敢于用 OPTIMIZER_TRACE 深挖,你就能解决 90% 以上的日常 SQL 性能问题。