从全表扫描到毫秒响应:一次优化提升了150倍
从全表扫描到毫秒响应:一次优化提升了150倍

上周三凌晨两点,手机突然炸了。线上核心接口响应时间直接飙到10秒,用户投诉像潮水一样涌进来。我睡眼惺忪地打开电脑,找到那条罪魁祸首的SQL,跑了一遍Explain,发现type是ALL——全表扫描,扫描了180万行。加了一个联合索引,10秒变30毫秒。这种事我这几年碰过太多次了,每次都让我更加坚信一句话:不会看Explain的开发者,写出来的SQL就是埋在系统里的定时炸弹。今天这篇文章不讲理论,全部拿真实的Explain执行计划做前后对比,让你一眼看清楚优化前后的差距到底有多大。
数据库工程与Explain执行计划对比优化实战

一、Explain到底是个什么东西,为什么它这么重要
很多开发者写SQL有个坏习惯:能跑就行,快不快再说。等出了问题再去排查,排查完也说不清为什么改了之后就快了。这种靠运气写代码的方式,在小项目里或许能混过去,但数据量一上来,必然翻车。
Explain是MySQL提供的一个命令,你在任何一条SELECT语句前面加上它,MySQL就不会真的去执行查询,而是告诉你它打算怎么执行这条SQL。换句话说,Explain就是MySQL给你开的一张"体检报告单"。它会告诉你这条SQL用了哪个索引、扫描了多少行、有没有做额外的排序、有没有建临时表。
☆ 我给自己定了一条铁规矩:任何一条要上线的SQL,必须先跑Explain。type低于range的,直接打回重写,没有例外。
这不是矫情,是血泪教训。我之前带过一个实习生,写了一条查询订单的SQL,测试环境数据量只有几千条,跑得飞快。上了生产之后数据量涨到两百万,直接把数据库CPU干到95%。我让他跑了一遍Explain,type是ALL,扫描了两百多万行。他自己看完都傻了。

二、Explain核心字段拆解:看懂这张表你就赢了一半
下面我用一个真实场景来逐个拆解Explain输出的关键字段。
假设我们有一张订单表,结构如下:
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
status TINYINT NOT NULL COMMENT '订单状态:1待付款 2已付款 3已发货 4已完成',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
create_time DATETIME NOT NULL COMMENT '创建时间',
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
现在有一条查询语句,要查某个用户在2024年的已支付订单:
sql
SELECT id, order_no, amount, create_time
FROM orders
WHERE user_id = 10086
AND status = 2
AND create_time >= '2024-01-01 00:00:00';
跑一下Explain,结果如下:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders ALL idx_user_id,idx_status,idx_create_time NULL NULL 1850000 Using where
下面我把每个关键字段掰开了讲:
1、type:这是整张表中最核心的字段,代表MySQL访问表的方式。从最优到最差依次是:system > const > eq_ref > ref > range > index > ALL。上面这个例子是ALL,也就是全表扫描,最差的那一档。
2、possible_keys:优化器认为可以用到的索引。这里列了三个,但实际一个都没用上。
3、key:实际使用的索引。这里是NULL,说明没走任何索引。
4、rows:预估要扫描的行数。185万行,对于一条按user_id查询的语句来说,这个数字触目惊心。
5、Extra:额外信息。这里出现了Using where,说明MySQL在存储引擎层拿到数据之后,在服务层又做了一次过滤。如果出现Using filesort或Using temporary,那就更糟糕了,意味着额外做了排序或建了临时表。
☆ 实战中我有个快速判断标准:如果type不是range及以上,或者Extra里出现了Using filesort、Using temporary,这条SQL大概率有问题,必须动手优化。

三、优化前后Explain对比:差距大到让人头皮发麻
上面那条SQL为什么走了全表扫描?原因很简单:三个单列索引都无法完全匹配查询条件。user_id单独用可以,但加上status和create_time之后,优化器发现任何一个单列索引都覆盖不了全部条件,索性就不用了,直接全表扫描。
解决方案也很直接:建一个联合索引。
sql
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
建完索引之后再跑Explain:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders range idx_user_status_time idx_user_status_time 27 8 Using index condition
把两次Explain放在一起对比:
对比项 优化前 优化后
type ALL(全表扫描) range(范围扫描)
key NULL idx_user_status_time
rows 1850000行 8行
Extra Using where Using index condition
从185万行降到8行,扫描行数减少了二十多万倍。实际响应时间从10秒降到了30毫秒以内。这就是Explain对比的威力——你不需要猜SQL为什么慢,Explain直接把答案拍在你脸上。

四、联合索引的最左前缀原则:这个搞不懂,索引白建
上面建的联合索引idx_user_status_time (user_id, status, create_time)之所以有效,靠的就是最左前缀原则。
这个原则的意思是:联合索引在匹配查询条件时,必须从最左边的字段开始依次匹配,一旦中间断了,后面的字段全部失效。
举个例子,索引是(user_id, status, create_time):
1、查询条件有user_id,索引生效 ✅
2、查询条件有user_id + status,索引完全生效 ✅
3、查询条件有user_id + status + create_time,索引完美匹配 ✅
4、查询条件只有status,索引不生效 ❌
5、查询条件只有create_time,索引不生效 ❌
所以建联合索引的时候,字段顺序非常有讲究。一般原则是:把区分度最高的、最常出现在等值查询里的字段放在最左边。什么叫区分度高?user_id有几百万个不同的值,区分度就高;status只有4个值,区分度就很低。所以user_id放最前面是对的。
☆ 我的习惯是:建索引之前,先把这个表上最高频的十条SQL拉出来,看它们的WHERE条件里都有哪些字段,然后按最左前缀原则来设计索引。这样建出来的索引,命中率最高。

五、覆盖索引:让查询连回表都省了
还是上面那个例子,如果我的查询只需要返回user_id、status、create_time这几个字段:
sql
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 10086
AND status = 2
AND create_time >= '2024-01-01 00:00:00';
这时候Explain的Extra字段会出现Using index,说明用了覆盖索引。什么意思?就是查询需要的所有字段都在索引树上,MySQL根本不需要回表去查聚簇索引,直接从二级索引上就把数据拿全了。
回表这个操作,很多人不当回事。InnoDB的二级索引里只存了索引字段的值和主键ID,真正的行数据在聚簇索引里。所以如果查询的字段不在二级索引里,MySQL就得拿着主键ID再去聚簇索引里查一遍,这就是回表。扫描1000行就要回表1000次,在高并发场景下这个开销非常可怕。
覆盖索引场景下的Explain结果:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders range idx_user_status_time idx_user_status_time 27 8 Using index condition; Using index
注意最后那个Using index,这就是覆盖索引的标志。看到它,就说明这条SQL已经优化到位了。
☆ 实战中我有个小技巧:写完SELECT之后,先看一眼查询的字段是不是都在联合索引里。如果不是,要么把字段加到索引里做成覆盖索引,要么想想这条查询是不是真的需要那么多字段。很多时候前端根本用不到那么多数据,是后端图省事一次全查出来了。

六、那些让索引失效的坑:建了索引也不一定快
很多人觉得建了索引就万事大吉了,其实不然。我在实际项目中遇到过太多"明明建了索引却没生效"的情况,下面这几个坑你一定要避开:
1、对索引字段使用函数:
sql
-- 索引失效,因为对create_time用了YEAR函数
WHERE YEAR(create_time) = 2024
-- 索引生效,直接用范围查询
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'
2、隐式类型转换:
sql
-- user_id是BIGINT,这里传了字符串,索引失效
WHERE user_id = '10086'
-- 改成数字,索引生效
WHERE user_id = 10086
3、LIKE以通配符开头:
sql
-- 索引失效
WHERE nickname LIKE '%三'
-- 索引生效
WHERE nickname LIKE '张%'
4、使用NOT IN或!=:
sql
-- 索引大概率失效
WHERE status != 2
-- 改成IN,索引可以生效
WHERE status IN (1, 3, 4)
5、OR连接的条件中只要有一个字段没索引,就可能全表扫描:
sql
-- user_id有索引,但status没有,整个查询可能走全表扫描
WHERE user_id = 10086 OR status = 2
☆ 这些坑看起来简单,但在实际业务代码里出现的频率非常高。我的做法是写完SQL之后一定要跑一遍Explain确认索引确实被用到了。养成这个习惯,能帮你避免80%的性能问题。

七、一个完整的查询优化案例:从Explain对比到上线
最后讲一个我在上家公司做过的真实优化案例。
背景:我们有一个运营后台的报表接口,需要查询某个时间段内所有已完成订单的汇总数据。原始SQL如下:
sql
SELECT DATE(create_time) AS order_date,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE status = 4
AND create_time >= '2024-01-01'
AND create_time < '2025-01-01'
GROUP BY DATE(create_time)
ORDER BY order_date DESC;
原始Explain结果:
id select_type table type possible_keys key rows Extra
1 SIMPLE orders ALL idx_status,idx_create_time NULL 2100000 Using where; Using temporary; Using filesort
问题非常严重:全表扫描210万行,还出现了Using temporary和Using filesort,说明MySQL又建了临时表又做了文件排序,性能差到了极点。
优化过程:
1、首先解决GROUP BY DATE(create_time)的问题。对create_time用了函数,索引肯定失效。把查询改成按天分组,利用索引的范围扫描:
sql
SELECT create_time,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE status = 4
AND create_time >= '2024-01-01 00:00:00'
AND create_time < '2025-01-01 00:00:00'
GROUP BY create_time
ORDER BY create_time DESC;
2、建立联合索引:
sql
ALTER TABLE orders ADD INDEX idx_status_time_amount (status, create_time, amount);
3、优化后的Explain:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders range idx_status_time_amount idx_status_time_amount 12 8500 Using index condition
对比数据:
对比项 优化前 优化后
type ALL range
扫描行数 210万行 8500行
Using temporary 有 无
Using filesort 有 无
响应时间 12秒+ 80毫秒
从12秒降到80毫秒,提升了150倍。而且因为用了覆盖索引,连回表都省了。
☆ 这里有个细节很多人会忽略:BETWEEN '2024-01-01' AND '2024-12-31'实际上包含了2024-12-31 23:59:59这一秒的所有数据,但如果create_time字段有毫秒精度,可能会漏掉最后一秒的部分数据。用>=和<的写法更加严谨,也更容易让索引生效。

八、写在最后
SQL优化这件事,说到底就三板斧:看Explain找问题,建索引提速度,改语句消浪费。听起来简单,但要真正做好,需要大量的实战积累。
我的建议是:从今天开始,每写一条SQL,都跑一遍Explain。一开始你可能看不懂,没关系,先把type和rows这两个字段盯紧了。type低于range的,回去改;rows超过一万的,回去改。坚持一个月,你对SQL性能的感觉就完全不一样了。
别觉得优化是DBA的事。写SQL的人不懂优化,就像开车的人不懂引擎一样,迟早要抛锚。Explain就是你的仪表盘,学会看它,你才能真正掌控这台机器。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)