500万数据的慢SQL,我只改了一行索引就起飞了
你有没有经历过这样的场景:一条SQL语句在测试环境跑得飞快,上线之后却把整个数据库拖垮了?这种"水土不服"的背后,往往藏着你根本没看过的执行计划。今天这篇文章,我会用真实案例带你从Explain分析入手,一步步拆解SQL优化的核心套路,看完你会发现,所谓的"调优高手"不过是把这些细节做得更扎实而已。
一、SQL优化为什么总被忽视
很多开发者在写SQL的时候,第一反应是"能跑就行"。尤其是业务初期,数据量不大,随便写条语句也能秒出结果。但随着业务增长,数据量从几万涨到几百万甚至上千万,原本"能跑"的SQL就变成了"能跑但要命"。
☆ 数据库层面的性能瓶颈,80%以上都跟SQL写法有关
☆ 索引不是越多越好,用错了反而比没索引还慢
☆ Explain是最被低估的调优工具,大部分人只看了一眼就关掉了
我见过太多团队花大价钱升级服务器、加内存,结果问题出在一条没加索引的关联查询上。说白了,SQL优化的性价比远高于堆硬件。接下来我们就从最基础的Explain说起,聊聊怎么把一条慢SQL一步步优化到极致。
二、用Explain看透SQL的执行真相
Explain是MySQL提供的执行计划分析工具,它不会真正执行你的SQL,而是告诉你数据库打算怎么执行这条语句。很多人用Explain只看type字段,其实每个字段都有它的意义。
我们先看一个实际案例。假设有一张订单表orders,大概有500万条数据:
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_user_status (user_id, status),
INDEX idx_create_time (create_time)
);
现在有这样一条查询:
sql
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345 AND status = 1
ORDER BY create_time DESC
LIMIT 10;
执行之后你会得到类似下面的结果:
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE orders NULL ref idx_user_status idx_user_status 17 const,const 320 100.00 Using where; Using index; Using filesort
重点看这几个字段:
1、type:显示为ref,说明用到了非唯一索引进行查找,这是一个还不错的结果。如果是ALL,那就意味着全表扫描,直接报警。
2、key:实际使用的索引是idx_user_status,说明优化器选择了联合索引,这步没问题。
3、rows:预估扫描320行。对于500万的表来说,这个数字还算可以接受。
4、Extra:这里出现了"Using filesort",这是个危险信号。说明虽然用了索引找数据,但排序操作没有利用索引完成,额外触发了文件排序。这就是性能隐患所在。
三、索引策略示例:联合索引的顺序到底怎么排
上面那个案例里,索引idx_user_status是(user_id, status)的顺序。很多人会问:为什么不是(status, user_id)?
这里涉及到一个核心原则:联合索引遵循最左前缀匹配原则,索引列的顺序直接决定了查询能否命中索引。
我们来做个对比:
sql
-- 场景一:按user_id查询,status作为过滤条件
SELECT * FROM orders
WHERE user_id = 12345 AND status = 1;
-- 索引(user_id, status) → 完美命中
-- 索引(status, user_id) → 只能用到status部分,user_id走不了索引
-- 场景二:只按status查询
SELECT * FROM orders WHERE status = 1;
-- 索引(user_id, status) → 走不了索引,因为最左列user_id没出现
-- 索引(status, user_id) → 可以命中
所以索引列的排列顺序,必须根据实际业务查询的频率来决定。一般的排布思路是:
1、等值查询的列放在最前面,比如user_id = xxx这种
2、范围查询的列放在后面,比如create_time > '2024-01-01'
3、排序用的列尽量放在索引的尾部,这样可以避免filesort
回到刚才的案例,我们的查询条件是user_id和status都是等值,但还有一个ORDER BY create_time。这意味着现有的联合索引无法覆盖排序需求。优化方案就是把索引改成:
sql
ALTER TABLE orders DROP INDEX idx_user_status;
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
改完之后再看Explain:
id select_type table type key rows Extra
1 SIMPLE orders ref idx_user_status_time 320 Using where; Using index
注意看Extra字段,"Using filesort"消失了,变成了"Using index"。这说明排序也走了索引,整个查询变成了覆盖索引扫描,性能提升非常明显。
四、查询优化案例:从3秒到30毫秒的真实调优过程
下面这个案例是我之前在一个电商项目中遇到的真实问题。
业务方反馈:用户中心的"我的订单"页面加载特别慢,有时候要等三四秒。我拿到SQL一看:
sql
SELECT o.id, o.amount, o.create_time, u.nickname
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status IN (1, 2, 3)
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 20;
先跑一下Explain看看问题出在哪:
id select_type table type possible_keys key rows Extra
1 SIMPLE o ALL idx_create_time NULL 4800000 Using where; Using filesort
1 SIMPLE u eq_ref PRIMARY PRIMARY 1 NULL
问题很明显:
1、orders表走了全表扫描,480万行全部扫了一遍
2、status用的是IN条件,但索引idx_create_time只包含create_time,status完全没用上索引
3、排序也触发了filesort
优化步骤如下:
1、首先给orders表加一个更合适的联合索引,把status和create_time都放进去:
sql
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
2、把IN查询改成等价的写法,让优化器更容易选择索引:
sql
SELECT o.id, o.amount, o.create_time, u.nickname
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE (o.status = 1 OR o.status = 2 OR o.status = 3)
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 20;
3、进一步优化,考虑到status只有三个值,且create_time是范围查询,我们把索引顺序调整为(create_time, status),因为范围查询放在联合索引中间会导致后面的列失效:
sql
ALTER TABLE orders DROP INDEX idx_status_time;
ALTER TABLE orders ADD INDEX idx_time_status (create_time, status);
改完之后再看Explain:
id select_type table type possible_keys key rows Extra
1 SIMPLE o range idx_time_status idx_time_status 15000 Using where; Using index
1 SIMPLE u eq_ref PRIMARY PRIMARY 1 NULL
rows从480万降到了1.5万,Extra里的filesort也没了。实际执行时间从3.2秒降到了28毫秒。
五、Explain对比:优化前后的差距有多大
为了让大家更直观地感受到优化效果,我把前后两次Explain的关键指标做了一个对比表:
对比项 优化前 优化后 变化
扫描行数 4800000 15000 降低99.7%
访问类型 ALL(全表扫描) range(范围扫描) 质的提升
是否用到索引 否 是 从无到有
是否触发filesort 是 否 消除排序开销
执行时间 3.2秒 28毫秒 提升约114倍
这个案例说明一个道理:很多时候性能问题不是数据量的问题,而是你的SQL和索引没有配合好。
六、几个容易踩坑的SQL优化细节
1、不要在索引列上做函数运算。比如WHERE YEAR(create_time) = 2024,这会让索引直接失效。应该改成create_time >= '2024-01-01' AND create_time < '2025-01-01'。
2、LIKE查询的百分号不要放在最前面。WHERE name LIKE '%张%'是没法走索引的,但WHERE name LIKE '张%'可以走。
3、尽量避免SELECT *。只查需要的字段,尤其是有TEXT或BLOB字段的表,SELECT *会把这些大字段也拉出来,严重影响IO性能。
4、JOIN的时候,被驱动表(右边那张表)一定要有索引。比如A JOIN B,B表的关联字段必须有索引,否则会触发NLJ(嵌套循环连接)的全表扫描。
5、LIMIT分页在大偏移量时也会变慢。比如LIMIT 1000000, 20,数据库要先扫描1000020行再丢弃前1000000行。可以用WHERE id > 上一页最后一条ID的方式来优化。
七、总结:SQL优化是一种思维方式
SQL优化不是背几条规则就够了,它更像是一种思维方式。你需要习惯在写完SQL之后看一眼Explain,需要理解索引的底层原理,需要知道优化器是怎么做选择的。
☆ 工具是死的,思路是活的。Explain给你的是数据,怎么解读靠的是经验
☆ 优化没有银弹,每个案例都要具体分析,但核心逻辑就那么几条
☆ 养成好习惯比记住多少技巧都管用
当你能从Explain的一行结果里快速定位问题、给出方案的时候,你就已经超越了大部分开发者。希望这篇文章能帮你在SQL调优的路上少走一些弯路。
💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~