别再写全表扫描了!四步教你搞定 90% 的慢查询
你有没有遇到过这种场景:线上接口突然变慢,用户疯狂投诉,DBA 让你赶紧查 SQL,你盯着 Explain 结果看了半天,依然不知道问题出在哪里?别慌,这篇文章就是写给你的。我会用真实的业务案例,手把手带你从 Explain 的每一个字段入手,一步步把一条 8 秒的慢查询优化到 50 毫秒以内。不讲虚的,全是干货,看完你就能直接上手改。
一、慢查询的根源:为什么你的 SQL 跑不快
很多开发同学在写 SQL 的时候,习惯"先跑通再说",根本不关心执行计划。等到数据量上来了,问题就爆发了。其实慢查询的原因无非就那么几个,但真到排查的时候,大多数人却抓不住重点。
1、全表扫描是最常见的杀手。当 WHERE 条件里的字段没有索引,MySQL 只能一行一行地扫整张表,数据量一过十万,性能直接崩盘。
2、索引用了但没用对。比如你给一个状态字段建了索引,但这个字段只有 0 和 1 两个值,区分度极低,优化器直接选择全表扫描反而更快。
3、JOIN 顺序不合理。多表关联的时候,MySQL 优化器会自动选择驱动表,但有时候它的选择并不是最优的,尤其是在数据量不均匀的情况下。
4、隐式类型转换。这是最容易被忽略的问题。比如字段是 varchar 类型,你却用数字去比较,索引直接失效。
举个最典型的例子,下面这条 SQL 在我们项目里曾经让接口响应超过 8 秒:
sql
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2024-01-01'
AND o.status = 1
ORDER BY o.id DESC
LIMIT 100;
这条语句看起来很普通,但在 orders 表有 500 万数据的情况下,跑一次要 8 秒多。问题到底出在哪?我们用 Explain 来看。
二、Explain 到底怎么看?别只盯着 type 列
很多人学 Explain 只知道看 type 是不是 ref 或者 range,但实际上 Explain 的每一个字段都有它的含义。下面我把最关键的几个字段拆开来说。
字段名 含义 重点关注什么
id 查询的执行序号 id 越大,执行越靠后;id 相同说明是同一步
select_type 查询类型 看到 DERIVED 或 SUBQUERY 要警惕
table 当前访问的表 关注驱动表是哪张
type 访问类型 从好到差:system > const > eq_ref > ref > range > index > ALL
possible_keys 可能用到的索引 看看有没有漏掉的索引
key 实际使用的索引 和 possible_keys 对比,看是否选对了
key_len 索引使用的字节数 越短越好,说明用到了前缀索引
rows 预估扫描行数 这个数字越小越好,但只是预估
Extra 额外信息 看到 Using filesort、Using temporary 就要注意了
回到刚才那条慢 SQL,我们看一下它的 Explain 结果:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o ALL idx_create_time NULL NULL 5123847 Using where; Using filesort
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 1 NULL
问题非常明显:orders 表的 type 是 ALL,也就是全表扫描,而且 Extra 里出现了 Using filesort,说明排序也没用上索引。possible_keys 显示 idx_create_time 是可用的,但 key 是 NULL,说明优化器压根没用这个索引。
三、优化实战:四步把 8 秒压到 50 毫秒
知道了问题在哪,接下来就是动手改。我把整个优化过程分成四步,每一步都有明确的目的。
1、先解决排序问题。原 SQL 按 o.id DESC 排序,但 id 本身没有和过滤条件组合成联合索引。我们把 idx_create_time 改成联合索引:
sql
ALTER TABLE orders DROP INDEX idx_create_time;
ALTER TABLE orders ADD INDEX idx_ctime_status (create_time, status, id);
这个索引的设计思路是:create_time 和 status 是等值和范围查询的字段,放在最前面;id 放在最后是为了覆盖 ORDER BY,避免 filesort。
2、再看 JOIN 的驱动表。原 SQL 用 orders 做驱动表,但 orders 有 500 万行,而 users 只有几万行。虽然 users 走了 eq_ref,但 orders 的扫描成本太高。我们用 FORCE INDEX 强制让 orders 走新索引:
sql
SELECT * FROM orders o FORCE INDEX(idx_ctime_status)
LEFT JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2024-01-01'
AND o.status = 1
ORDER BY o.id DESC
LIMIT 100;
3、优化后再看 Explain:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o range idx_ctime_status idx_ctime_status 12 18432 Using index condition
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 1 NULL
可以看到:type 从 ALL 变成了 range,rows 从 512 万降到了 1.8 万,Extra 里的 Using filesort 也消失了。
4、最终结果:查询时间从 8.2 秒降到了 47 毫秒,提升了将近 175 倍。
四、更多容易踩的坑:这些细节决定成败
优化不是改完索引就完了,下面这些细节很多人都会忽略,但在高并发场景下,它们才是真正的瓶颈。
1、LIMIT 偏移量过大的问题。比如 LIMIT 1000000, 10,MySQL 需要扫描 1000010 行然后丢掉前 100 万行。这种场景建议用游标或者记录上次的 ID:
sql
-- 不推荐
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 推荐:用上次最后一条的 ID
SELECT * FROM orders WHERE id > 上次最后的ID ORDER BY id LIMIT 10;
2、COUNT(*) 和 COUNT(1) 的区别。在 InnoDB 引擎下,COUNT() 会被优化成扫描最小的二级索引,而 COUNT(1) 和 COUNT(列) 反而可能更慢。所以别纠结了,直接用 COUNT()。
3、OR 条件导致索引失效。下面这条 SQL 看起来索引都能用,但实际上只会走一个:
sql
SELECT * FROM orders WHERE user_id = 100 OR status = 1;
改成 UNION ALL 往往更快:
sql
SELECT * FROM orders WHERE user_id = 100
UNION ALL
SELECT * FROM orders WHERE status = 1 AND user_id != 100;
4、函数操作导致索引失效。这是最经典的坑:
sql
-- 索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;
-- 改成范围查询,索引生效
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
五、建立自己的 SQL 优化 checklist
经过这么多年的实战,我总结了一套自己的排查清单,每次遇到慢 SQL 就按这个顺序过一遍,基本不会漏。
1、先跑 Explain,看 type 是不是 ALL 或者 index,如果是,优先考虑加索引或改索引。
2、看 Extra 有没有 Using filesort 或 Using temporary,有的话优先通过调整索引或改写 SQL 消除。
3、看 key_len,如果很短但 rows 很大,说明索引区分度不够,需要考虑联合索引。
4、检查 WHERE 条件里有没有函数、隐式转换、OR 等导致索引失效的写法。
5、如果单表优化已经到极限了,再考虑是不是需要改业务逻辑,比如分页方式、查询条件等。
☆ 记住一句话:最好的 SQL 优化,不是让数据库跑得更快,而是让查询的数据量变得更少。索引是手段,减少扫描才是目的。
六、写在最后
SQL 优化这件事,说难也难,说简单也简单。难的是你要理解 MySQL 优化器的决策逻辑,简单的是只要你养成看 Explain 的习惯,80% 的慢查询都能在开发阶段就被消灭。
不要等到线上出了问题才去查 Explain,那时候用户已经在骂人了。把优化的思维融入到日常写 SQL 的每一个习惯里,这才是真正的功力。
希望这篇文章能帮你在下次遇到慢 SQL 的时候,不再手足无措。如果你也有自己的优化案例,欢迎在评论区交流,咱们一起进步。
💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~