type
Post
status
Published
date
Dec 16, 2025
slug
mysql3
summary
tags
技术探索
category
icon
password
我以前学 MySQL 调优时,也以为慢 SQL 优化的核心就是一句话:
SQL 慢了,加索引。
这个想法不能说完全错。
但后来看到一类很常见的分页查询,才发现事情没有这么简单。
比如后台订单列表里有这样一条 SQL:
这条 SQL 第一眼看起来很普通。
它只是查询
status = 1 的订单,按照创建时间倒序排列,然后返回 20 条数据。很多人第一次看到它,都会有一个直觉:
最后不就返回 20 条吗?为什么会慢?
问题就出在这里。
MySQL 执行
LIMIT 100000, 20 时,通常不是直接跳到第 100001 条记录,然后取后面 20 条。它更可能是:
先找到前 100020 条记录再丢掉前 100000 条最后返回 20 条
也就是说,这条 SQL 慢的关键是:
为了返回这 20 行,MySQL 到底扫描了多少数据、做了多少排序、发生了多少回表?
这也是 MySQL 调优真正要解决的问题。
一、别被 LIMIT 20 骗了:返回少,不代表扫描少
比如这条 SQL:
表面上看,它只返回 20 条数据。
但 MySQL 为了找到这 20 条数据,可能要先处理大量满足
status = 1 的记录。如果没有合适的索引,MySQL 可能需要:
扫描大量订单数据找出status = 1的记录按照create_time排序跳过前 100000 条返回后 20 条
所以,深分页慢的本质是:
为了得到这 20 条数据,前面跳过了太多数据。
这时候如果你只盯着
LIMIT 20,就会误判 SQL 成本。真正应该看的,是扫描行数、排序成本和回表成本。
二、慢 SQL 先别急着加索引优化,先判断它慢在哪里
更合理的方式,是先判断这条 SQL 属于哪种慢。
一般可以先分成两类:
稳定慢偶发慢
1. 如果每次都慢,大概率是执行路径本身有问题
如果一条 SQL 每次执行都慢,比如不管什么时候查都要几秒,那通常要重点看:
扫描行数是不是太多有没有走合适的索引回表次数是不是太多ORDER BY有没有触发额外排序GROUP BY有没有产生临时表LIMIT深分页是不是跳过了大量数据
这种慢,通常和 SQL 写法、索引设计、查询条件、排序分页方式有关。
比如前面的订单分页 SQL,如果每次翻到很深的页码都慢,那它大概率不是偶发问题,而是深分页本身就重。
2. 如果平时很快,偶尔很慢,就不要只盯着索引
还有一种情况是:
同一条 SQL,大多数时候很快,偶尔突然慢几秒。
这时候问题不一定出在 SQL 写法本身。
可能是它在等锁。
也可能是数据库当时 IO 抖动。
也可能是有长事务、热点更新、脏页刷盘等问题。
比如一条更新订单状态的 SQL,本身通过主键更新,执行计划看起来完全没问题。
但如果另一条事务已经锁住了这行订单,而且迟迟不提交,那么这条 SQL 就只能等待。
这时候 SQL 慢,不是因为它没走索引,而是因为它被锁住了。
所以慢 SQL 调优的第一步,是先判断:
它是稳定慢,还是偶发慢?
这个判断会直接决定后面的排查方向。
三、看 EXPLAIN,是为了回答三个问题
定位到一条稳定慢的 SQL 后,下一步通常会看执行计划。
比如:
很多人学
EXPLAIN 时,会陷入一个误区:把每个字段都背下来。
比如
type、possible_keys、key、key_len、rows、filtered、Extra。这些字段当然有用。
但刚开始分析慢 SQL 时,不要把它当成字段背诵题。
你可以先用
EXPLAIN 回答三个最关键的问题。1. 有没有走合适的索引?
主要看
key。如果
key = NULL,说明这条 SQL 没有使用索引。如果表数据量很大,就要警惕全表扫描。
但要注意:
用了索引,不代表一定快。
因为索引可能区分度很低,也可能扫描了大量索引记录。
所以不能只看有没有走索引,还要继续看扫描行数。
2. 预计要扫描多少行?
主要看
rows。rows 表示优化器预估要扫描多少行。如果一条 SQL 最终只返回 20 行,但
rows 预估要扫描几十万行,那就说明它的执行成本可能并不低。这也是很多分页 SQL 慢的原因。
不是返回数据多,而是扫描和跳过的数据多。
3. 有没有额外排序或临时表?
主要看
Extra。如果看到:
说明 MySQL 不能直接利用索引顺序返回结果,需要额外排序。
注意,
Using filesort 不一定代表真的在磁盘上排序。它更准确的含义是:
MySQL 需要额外做一次排序,而不是直接按索引顺序返回。
如果看到:
说明执行过程中可能使用了临时表。
这在一些
GROUP BY、DISTINCT、复杂排序场景里比较常见。小数据量下问题可能不大。
但如果数据量很大,额外排序和临时表就可能成为主要瓶颈。
四、为什么加了索引还是慢?
很多人以为:
SQL 慢,加索引就好了。
但真实情况是:
有索引,不代表一定快。
比如订单表上有一个
status 索引:然后执行:
如果
status = 1 的订单占全表一半,那么这个索引的效果可能并不好。因为 MySQL 通过
status 索引找到大量记录后,还要根据主键回表查询整行数据。这时候它可能会觉得:
走索引再大量回表,还不如直接扫全表。
所以,判断一个索引有没有价值,重要的是看:
它能不能有效减少扫描行数?它能不能减少回表次数?它能不能避免额外排序?
五、SELECT * 可能会让回表成本变高
在 InnoDB 里,主键索引的叶子节点存的是整行数据。
普通二级索引的叶子节点存的是:
索引字段值 + 主键值
所以,如果通过二级索引查询数据,通常要分两步:
第一步:查二级索引,找到主键值第二步:根据主键值回到主键索引,查整行数据
第二步就是回表。
如果只回表几十次,问题不大。
但如果一条 SQL 通过二级索引扫描了几十万条记录,并且每条都要回表,那性能就会明显下降。
比如:
这里用了
SELECT *,意味着 MySQL 最后需要拿到完整行数据。如果分页很深,又发生大量回表,成本就会很高。
这也是为什么很多慢 SQL 优化时,会建议:
不要随便SELECT *。能只查必要字段,就只查必要字段。
因为返回字段越多,越容易让覆盖索引失效,也越可能增加回表成本。
六、联合索引不是只服务 WHERE,也可以服务 ORDER BY
很多人设计索引时,只盯着
WHERE 条件。比如看到:
就想到给
status 建索引。但对于这条 SQL 来说:
真正重要的不只是过滤
status,还包括:按create_time排序只取前 20 条
如果只有
status 索引,MySQL 可能先找到所有 status = 1 的订单,再对这些订单按 create_time 排序。如果
status = 1 的数据很多,排序成本就会很高。这时候更合理的索引可能是:
这样 MySQL 可以先通过
status 过滤,再利用 create_time 的索引顺序取数据。也就是说,联合索引不只是为了
WHERE。它还可以同时服务:
过滤排序分页覆盖索引
所以设计联合索引时,不要只背“最左前缀原则”。
更实用的思路是:
先看业务高频查询模式再看 WHERE 条件再看 ORDER BY、GROUP BY、LIMIT最后看返回字段能不能被覆盖
索引不是孤立设计的,而是围绕查询路径设计的。
七、深分页怎么优化?
现在回到最开始这条 SQL:
它的核心问题是:
offset 太大,MySQL 要跳过大量数据。
针对深分页,常见优化方式有两类。
1. 覆盖索引 + 延迟关联
如果业务必须保留
LIMIT offset, size 这种分页方式,可以考虑用覆盖索引加延迟关联。原来的 SQL 是:
问题在于,它可能一边扫描大量数据,一边产生大量回表。
可以先改成只查主键:
如果有合适的联合索引:
那么
id、status、create_time 都可以从索引里拿到。这一步就有机会避免大量回表。
拿到目标页的 20 个
id 后,再查完整数据:这就是延迟关联。
它的核心思想是:
先用尽量轻的索引扫描定位目标 id,再回表查询真正需要的 20 条完整数据。
这样可以减少大量无效回表。
但要注意,覆盖索引 + 延迟关联不是万能的。
它只是减少了回表成本,没有彻底消除深分页跳过大量数据的问题。
也就是说:
普通深分页:扫描大量记录 + 可能大量回表覆盖索引 + 延迟关联:扫描大量索引记录 + 少量回表
它比原写法更轻,但 offset 很大时,扫描大量索引记录的问题仍然存在。
2. 游标分页
如果业务不要求“任意跳到第 N 页”,而只是需要“上一页、下一页”,可以考虑游标分页。
比如上一页最后一条数据是:
下一页可以这样查:
对应索引可以设计为:
这里为什么要带上
id?因为
create_time 可能重复。如果只按照
create_time 排序,分页结果可能不稳定。加上
id 作为第二排序字段,可以保证排序稳定。游标分页的优势是:
不再从第 1 条开始跳过大量 offset 数据,而是从上一次的位置继续往后查。
所以它通常比深分页更适合大数据量列表。
但它也有局限:
不适合任意跳到第 N 页需要前端或接口保存上一页游标排序字段要稳定最好有匹配的联合索引
所以,深分页优化不是固定套路。
如果业务必须跳页,可以考虑覆盖索引 + 延迟关联。
如果业务只需要加载下一页,更推荐游标分页。
八、ORDER BY、GROUP BY 也可能是慢 SQL 的大头
除了深分页,排序和分组也是慢 SQL 里很常见的成本来源。
比如:
如果没有合适的索引,MySQL 可能要先找出所有
status = 1 的订单,再做额外排序。执行计划里可能出现:Using filesort
再比如:
如果数据量很大,MySQL 可能要处理大量记录,并产生临时表。
执行计划里可能出现:Using temporary
所以优化排序和分组时,常见思路是:
先用 WHERE 尽量减少参与排序和分组的数据量设计合适的联合索引,让索引同时服务过滤和排序避免返回不必要的字段避免对大结果集做无意义排序
很多时候,慢 SQL 是慢在“查出来之后还要排序、分组、丢弃”。
九、索引不是越多越好
说到这里,好像很多问题都可以通过索引优化。
但索引不是免费的。
每多一个索引,写入、更新、删除时都要维护它。
比如订单表如果有很多二级索引,那么每插入一条订单,不只是写入一条数据,还要维护多个索引结构。
所以索引会带来额外成本:
占用磁盘空间增加写入成本增加更新成本占用 Buffer Pool可能影响优化器选择执行计划
读多写少的表,可以适当用更多索引优化查询。
但写多的表,就要更谨慎。
尤其是订单、流水、日志这类数据量大、写入频繁的表,索引设计不能只考虑查询快不快,也要考虑写入能不能承受。
一个比较稳妥的原则是:
高频查询、核心接口、收益明显的索引值得建。低频查询、临时需求、收益不确定的索引要谨慎建。能用一个联合索引覆盖多个高频查询时,不要盲目堆很多单列索引。
十、SQL 写法也会影响索引效果
有时候不是索引没建,而是 SQL 写法让索引用不好。
比如对索引字段做计算:
这种写法不如改成:
再比如对时间字段做函数计算:
更推荐改成范围查询:
还有隐式类型转换。
如果
phone 字段是 varchar,却写成:MySQL 可能会发生隐式类型转换,导致索引效果变差。
更合理的写法是:
所以 SQL 调优不是只会建索引。
还要避免写出让索引难以发挥作用的 SQL。
十一、执行计划没问题,SQL 为什么还是会慢?
前面讲的大部分问题,都属于“稳定慢”。
也就是 SQL 每次执行都慢。
这类问题可以重点看执行计划、索引、扫描行数、排序和分页。
但还有一种情况:
EXPLAIN 看起来没什么问题,SQL 平时也很快,但偶尔就是会突然慢一下。
这时候就不要只盯着 SQL 本身了。
因为它可能慢在等待。
1. 有些慢 SQL,其实是在等锁
比如一个事务更新了某一行订单,但迟迟没有提交。
另一个事务也要更新这行订单,就只能等待。
这时候 SQL 慢,不是因为它扫描了很多行,而是因为它在等锁。
常见问题包括:
行锁等待热点行更新长事务死锁间隙锁MVCC 版本链过长
比如库存扣减、账户余额更新、订单状态更新,都可能出现热点行竞争。
这类问题不是单纯加索引就能解决的。
更合理的排查方向是:
看是否存在长事务看是否有锁等待看热点更新是否集中在少数几行看事务范围是不是太大看 SQL 是否因为索引不合理锁住了更多范围
尤其要注意一点:
索引不仅影响查询速度,也可能影响加锁范围。
如果更新条件没有走合适索引,MySQL 可能扫描更多记录,锁范围也可能变大。
2. 有些偶发慢,可能和数据库 IO 抖动有关
InnoDB 修改数据时,不会每次都立刻把数据页写回磁盘。
它会先修改 Buffer Pool 中的数据页,同时写 redo log。
这时候内存里的数据页已经变了,但磁盘上的数据页还没变。
这种页叫脏页。
脏页不能永远留在内存里。
在一些场景下,MySQL 需要把脏页刷回磁盘,比如:
Buffer Pool 空间不够redo log 空间压力变大后台线程定期刷脏页数据库正常关闭
如果某个时间点集中刷脏页,就可能造成磁盘 IO 抖动。
这时候 SQL 执行计划没有变化,但响应时间可能突然变长。
所以,如果你发现:
SQL 平时很快执行计划没变数据量没突然变大但偶尔会慢几秒
就要考虑它是不是慢在锁等待、长事务、脏页刷盘或磁盘 IO 抖动上。
这类问题要结合监控一起看,而不是只看
EXPLAIN。十二、调优不是凭感觉,最后一定要验证
很多调优失败,是因为没有验证。
比如你觉得加了索引一定会快,但实际可能出现:
查询快了,但写入变慢了优化器没有选择新索引执行计划变得不稳定索引太宽,占用大量 Buffer Pool某些查询变快,另一些查询变慢
所以调优之后,一定要对比优化前后的效果。
至少要看:
执行计划有没有变化扫描行数有没有下降是否减少了回表是否减少了 filesort 或 temporary接口耗时有没有下降数据库 CPU、IO 有没有改善写入成本是否可以接受
MySQL 调优最怕的是:
我感觉这样会快。
更好的方式是:
我能证明这样确实更快,而且副作用可接受。
十三、一张图总结慢 SQL 排查思路
讲到这里,再来看完整的排查链路就清楚多了。
你只要记住主线:
稳定慢,看执行路径。偶发慢,看等待和抖动。优化后,一定要验证。
总结:MySQL 调优真正调的是什么?
MySQL 调优是在找这条 SQL 的主要成本。
如果它扫描行数太多,就要看索引和查询条件。
如果它回表次数太多,就要考虑覆盖索引、减少返回字段,或者延迟关联。
如果它排序成本太高,就要看联合索引能不能同时服务
WHERE 和 ORDER BY。如果它是深分页,就要考虑延迟关联或游标分页。
如果它执行计划没问题,但偶尔慢,就要排查锁等待、长事务、热点行、脏页刷盘和 IO 抖动。
所以,真正靠谱的慢 SQL 优化思路应该是:
先定位慢 SQL。再判断它是稳定慢还是偶发慢。稳定慢就看执行计划、扫描行数、回表、排序、临时表和深分页。偶发慢就看锁等待、长事务、热点更新和数据库 IO 抖动。最后根据瓶颈选择索引优化、SQL 改写、分页改造或事务优化,并验证效果和副作用。
MySQL 慢 SQL 怎么优化?
更好的回答是:
我会先从慢查询日志或接口监控定位问题 SQL,然后判断它是稳定慢还是偶发慢。稳定慢就用 EXPLAIN 看执行计划,重点分析有没有走合适索引、扫描行数是不是太多、有没有大量回表、排序、临时表和深分页;偶发慢就排查锁等待、长事务、热点行更新、脏页刷盘和 IO 抖动。最后再根据具体瓶颈选择联合索引、覆盖索引、SQL 改写、分页优化或事务优化,并对比优化前后的执行计划、扫描行数、耗时和资源消耗。
这才是 MySQL 调优真正应该具备的思路。
分享
