面试能背八股文,生产却还在全表扫描:SQL优化的八个反直觉真相

2026-08-11 8 0

昨天帮朋友看一个接口超时问题,打开代码一看:SELECT * FROM orders WHERE status = 1 AND DATE_FORMAT(create_time, "%Y-%m") = "2026-08"。我当场血压就上来了。

这不是个案。我见过太多面试时能把索引原理讲得头头是道、join算法倒背如流的开发者,一写代码就原形毕露。本文不教你背八股文,只讲我在生产环境里亲眼见过的八个反直觉真相。


1. 你以为 EXPLAIN 够了,其实你看的索引可能根本没用

很多人跑个 EXPLAIN 看到 Using index 就觉得稳了。我只能说Too young。

看这个查询:

SELECT id, name FROM users WHERE age + 1 = 30;

你猜 EXPLAIN 显示啥?可能还真显示走了索引。但实际上,因为对索引列做了运算,数据库要遍历每一行计算 age + 1 才能判断。索引确实被"用到"了,但效率等于没用的那种。

正确姿势:永远不要在索引列上做计算。改成 WHERE age = 29,这才叫走索引。

同样的坑还有函数:WHERE YEAR(create_time) = 2026WHERE LOWER(name) = "zhang"。索引列套函数,索引直接废掉。


2. OR 是性能杀手,但你可能用错了场景

大家都知道 OR 效率低对吧?但你知道为什么低吗?

SELECT * FROM orders WHERE user_id = 100 OR status = 1;

这条语句如果 user_id 有索引而 status 没有,数据库会先扫描 user_id 的索引树找到 100,再老老实实全表扫 status = 1 的数据,最后合并。走两个路径,没有 union 合并优化的话,就是慢。

但更骚的操作是这个:

SELECT * FROM orders WHERE id IN (1, 2, 3) OR id IN (4, 5, 6);

有人觉得这样写挺聪明的。错!MySQL 对 IN 的优化是把括号里的值拆开成 union 处理的,但加了 OR 之后,优化器可能直接放弃治疗,走全表扫描。

正确姿势:用 UNION 替代 OR,或者确保 OR 连接的字段都有合适的索引。


3. 你以为分页越大越爽,其实 LIMIT offset, count 是性能毒药

很多前端喜欢搞"加载更多",一次查20条,用户往下滑又查20条。听起来没问题,但当 offset 到了几十万的时候,你就知道什么叫灾难了。

原理很简单:LIMIT 500000, 20 的意思是"先数出前500000行,然后返回接下来的20行"。数据库要把这50万行都扫一遍才能定位到你要的20条。

正确姿势:用游标分页(keyset pagination)。

-- 传统方式(慢)
SELECT * FROM orders ORDER BY id LIMIT 500000, 20;

-- 游标方式(快)
SELECT * FROM orders WHERE id > 500000 ORDER BY id LIMIT 20;

原理:利用主键索引直接定位,根本不需要数前面的行。如果你的分页慢,先看看是不是还在用 offset 方式。


4. JOIN 顺序不是你说了算,是优化器说了算

很多人在写 JOIN 的时候会纠结顺序:A JOIN BB JOIN A 有区别吗?理论上没区别,执行上有区别,但区别不一定是你想要的那个。

MySQL 优化器会根据统计信息自动选择 join 顺序。它认为哪个表小就先扫哪个,然后用它的索引去关联大表。听起来很美好对吧?

问题来了:如果你的表统计信息过期了(数据量发生巨大变化但 ANALYZE 没跑),优化器就会做出错误判断,把小表当大表,灾难就发生了。

我见过一个生产事故:一张1000万行的表因为清理任务跑完只剩10万行,但统计信息没更新,优化器判断它很大,于是在 JOIN 时选错了顺序,整个查询从2秒变成2分钟。

正确姿势:定期跑 ANALYZE TABLE,或者在 SQL 里用 STRAIGHT_JOIN 强制指定顺序(慎用,这是告诉优化器"你不行我来")。


5. 字符串索引的坑:前缀索引可能是你自己埋的雷

为了省空间,很多人喜欢建前缀索引:

ALTER TABLE users ADD INDEX idx_email (email(10));

看起来很合理,email 前10个字符就能区分大部分用户了嘛。但问题来了:

SELECT * FROM users WHERE email = "verylongemail@example.com";

这条查询不会用到前缀索引,因为前缀索引只匹配前缀,后面的字符不一样,你精确查整个字符串,优化器觉得直接全表扫可能更快,就不用索引了。

另一个坑:LIKE 'zhang%' 能用到前缀索引,但 LIKE '%zhang%' 不能。这大家都知道,但你知道 LIKE 'zhang' 能不能用吗?答案是能,但和 = 效率一样低,因为 LIKE 在没有通配符时就是精确匹配,不会利用前缀索引的特性。

正确姿势:如果你的字符串字段需要精确查询,直接用完整索引,别省这点空间。


6. NULL 不是"空",是第三种状态,索引它等于索引了个寂寞

IS NULL 能不能用索引?这是个经典面试题,答案通常是"能"。

但现实是:如果表中 NULL 比例很高,你的 WHERE status IS NULL 可能比全表扫描还慢。为什么?因为索引里存的是值,NULL 也占空间,当 NULL 多到一定程度,索引反而变成了负担。

MyISAM 引擎尤其明显:NULL 值不参与索引统计,但查询时又不得不处理这些行。

更坑的是这个:

SELECT * FROM orders WHERE status != 1;

不等于不走索引?不,不走。如果 status 只有 0 和 1 两种值,!= 1 意味着查所有 status = 0 的数据。如果有部分 NULL,优化器可能判断"反正就两个值,直接扫比用索引快",然后真的全表扫。

正确姿势:尽量给字段设默认值,避免大量 NULL。用 NOT NULL 约束。


7. 批量插入不是insert越多越好,5.1之后要这样玩

很多人知道用事务包裹批量插入能提速:

BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO orders (...) VALUES (...);
...
COMMIT;

但你知道吗?在 MySQL 5.1 之后,单条 INSERT 改成多条 VALUES 可以显著减少网络开销和解析开销:

INSERT INTO orders (...) VALUES (...), (...), (...), (...);

一次插入1000行比1000次插入1行快多少?实测大概是10-50倍的差距。原因:每次插入都要建立连接、发包、解析语法、事务提交。多条 VALUES 合并后,这些开销只要一次。

但要注意:MySQL 有一个 max_allowed_packet 限制,如果一条 SQL 太大,会被截断或者报错。一般建议单条 SQL 不超过 1MB,实际操作中建议500-1000条一批。

还有一点:InnoDB 在插入时会有自适应哈希索引、插入缓冲区等机制,如果你的表有太多索引,插入速度会显著下降。所以批量导入时,可以先删掉非必要索引,导完再重建。


8. 覆盖索引是银弹,但你可能正在滥用它

覆盖索引(Covering Index)是指查询的所有字段都包含在索引中,不需要回表。这确实能提速,因为只扫描索引树就能拿到数据。

但很多人滥用它:建一个包含十几个字段的联合索引来"优化"一个查询。结果呢?索引变大,插入变慢,维护成本上升,而且一旦查询条件变化,索引就不适用了。

更坑的是:覆盖索引只对当前查询有效。如果你的应用里这个表的查询有几十种形态,你不可能为每种查询都建一个覆盖索引。

正确姿势:覆盖索引只用于高频、稳定的查询场景。普适优化还是回到合理的字段设计 + 合适的单字段索引。


写在最后

SQL 优化不是背背八股文就能会的。真正的优化能力来自于:对数据库原理的深刻理解 + 对业务场景的清醒认知 + 对代码实现的仔细审查。

下次再有人问你怎么优化 SQL,别急着说"加索引"。先看看他的查询有没有函数、有没有 OR、有没有深分页。很多时候,最快的优化是不写那条 SQL

比如我开头提到的那个查询,DATE_FORMAT(create_time, "%Y-%m") = "2026-08",改成 create_time >= "2026-08-01" AND create_time < "2026-09-01" 之后,查询时间从 8 秒变成 80 毫秒。索引还是那个索引,变的是写法。

记住:你控制不了数据库的优化器,但你能控制你的 SQL 怎么写。共勉。

相关文章

数据库连接池:你好好的应用,怎么就开始抽风了?
RESTful API 设计翻车现场:那些年我们一起写过的烂接口
SQL优化这条路,走过的人都说”太难了”
为什么你的”整洁代码”正在悄悄杀死系统性能
你的服务正在慢性自杀:熔断器才是最后的救命稻草
异步编程:为什么你的”async”形同虚设?

发布评论