SQL优化:那些你以为用对了但偷偷在拖慢你系统的索引潜规则

2026-09-02 11 0

SQL优化:那些你以为用对了但偷偷在拖慢你系统的索引潜规则

面试的时候问候选人:"什么情况会导致索引失效?"

十个人里有九个会回答:"LIKE 以通配符开头就会失效!"

然后你再追问一句:"还有呢?"

空气突然安静。

这是一个有意思的现象——大多数后端工程师对索引的理解,停留在背面试题的程度。真正线上跑着几千万数据的系统,SQL 跑得慢,原因往往不是"没用索引",而是"你以为在用索引,实际上数据库偷偷换成了全表扫描"。

今天聊几个真正干过生产的人才知道的索引潜规则。


潜规则一:OR 不是你想的那样

大部分人以为 OR 条件很简单:A OR B,只要有一个字段有索引就能走。

错。

看这个例子:

SELECT * FROM orders 
WHERE user_id = 12345 
   OR status = 'paid';

user_id 有索引,status 没索引。MySQL 的优化器会怎么做?它可能发现 status 没索引,直接放弃走 user_id 的索引,改成全表扫描。

因为 OR 的语义是"两个条件要同时满足",当其中一个条件没有索引时,优化器觉得全表扫描可能更快,就直接全表扫了。你还以为 user_id = 12345 这条在走索引。

正确的做法是拆成 UNION:

SELECT * FROM orders WHERE user_id = 12345
UNION ALL
SELECT * FROM orders WHERE status = 'paid' AND user_id != 12345;

这样 user_id 的索引可以被两次独立使用,而不是被 status 这个没索引的字段拖累。

当然,如果 status 也有索引,可以直接走 index merge,MySQL 8.0+ 会自动优化。但当你接手一个老系统,status 字段刚加还没建索引的时候,这个 OR 分分钟让你的查询变成噩梦。


潜规则二:NULL 不是"空",是另一个值

SQL 里的 NULL 表示"未知",不是"空字符串",也不是"0"。

这个概念坑了无数人。

SELECT * FROM users WHERE phone IS NULL;  -- 找没有电话的用户
SELECT * FROM users WHERE phone = '';     -- 找电话是空字符串的用户
SELECT * FROM users WHERE phone = '0';    -- 找电话是字符串'0'的用户

这三个查询完全不同。

但更坑的是:大多数数据库对 NULL 值的索引处理是特殊的。很多优化器在遇到 NULL 条件时,会认为"这个值不存在于索引中",从而跳过索引。

-- phone 字段有索引,但这条查询可能不走索引
SELECT * FROM users WHERE phone IS NULL;

-- 改成这样,走索引的可能性大得多
SELECT * FROM users WHERE phone = '' OR phone IS NULL;

生产环境中,如果你有很多业务逻辑依赖 NULL 判断,建议统一改成有明确意义的默认值(比如"未填写"、"-1"),既避免了 NULL 的歧义,也让索引更可靠。

这不是吹毛求疵,是真的有人在凌晨三点被"这条 SQL 怎么这么慢"惊醒过。


潜规则三:类型不匹配,索引直接报废

这个问题低级到很多人不信,但它每天都在生产环境里发生。

-- user_id 是 bigint 类型
SELECT * FROM orders WHERE user_id = '12345';  -- 字符串
SELECT * FROM orders WHERE user_id = 12345;     -- 数字

这两条 SQL,长得几乎一样,但性能可能差几十倍。

第一条里,user_id = '12345',数据库需要把每一行的 user_id 从 bigint 转成字符串,再和'12345'比较。类型转换导致索引无法使用,直接全表扫描。

第二条才是正确的写法。

这种问题最常出现在:动态 SQL 拼接、ORM 自动生成、接口参数没有做类型校验的地方。user_id 传进来是个字符串,你就直接塞进 SQL 里了,线上跑着跑着越来越慢,你还以为是数据量大了。

血泪教训:后端接收到的所有参数,进 SQL 之前一定要做类型校验。宁可多写两行代码,也别让数据库做隐式类型转换。


潜规则四:函数用在索引列上,等于给索引判了死刑

这条最经典,但还是有人踩:

-- 不会走索引
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
SELECT * FROM orders WHERE DATE_FORMAT(create_time, '%Y-%m') = '2026-09';
SELECT * FROM users WHERE LEFT(phone, 3) = '188';

-- 正确做法:改写成范围查询
SELECT * FROM orders WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01';
SELECT * FROM orders WHERE create_time >= '2026-09-01' AND create_time < '2026-10-01';

当你对索引列做任何函数操作,数据库就无法利用 B+Tree 的有序性,只能老老实实逐行扫描。YEAR() 在索引列上,就是索引杀手。

解决方案永远是:把函数操作移到右边,让索引列保持原样出现在比较操作中。

如果你真的需要频繁按 YEAR 查询,那应该建立函数索引(MySQL 8.0+ 支持),或者加一个冗余列专门存年份,用这个列做查询。


潜规则五:索引不是越多越好,写入性能会被反噬

很多人有个误解:查询慢?加索引啊!

加索引确实能让查询快。但每加一个索引,INSERT/UPDATE/DELETE 的性能就会慢一点。

因为数据写入时,数据库需要同时维护所有索引的 B+Tree 结构。十个索引的表,每次写入相当于要更新十棵树。

对于写多读少的场景(大日志表、流水表),索引太多会让写入成为瓶颈。曾经有个项目,流量日志表建了八个索引,QPS 压测怎么也上不去。后来砍到两个索引,写入性能直接翻了三倍。

所以:

  • 读多写少的表可以适当多建索引
  • 写多读少的表严格控制索引数量,最多两到三个
  • 定期检查慢查询日志,删除那些长期没人用的索引

索引是用来加速读的,不是越多越好的。


最后一条最重要的潜规则

所有这些规则,都只是经验。

真正靠谱的做法只有一个:用 EXPLAIN 看执行计划

EXPLAIN SELECT * FROM orders WHERE user_id = 12345;

看 key 字段,看 rows 字段,看 type 字段。

key 是 NULL?全表扫描。

rows 比你预期的大?可能有问题。

type 是 ALL?全表扫描。type 是 range?范围查询。type 是 ref?索引查找。

别靠猜,别靠背面试题。线上的数据分布和你想象的不一定一样,最靠谱的是让数据库告诉你它在做什么。

以上五条,是我觉得大多数教程不会讲、但生产环境天天见的索引潜规则。希望你看完之后,线上慢查询再也不是靠重启数据库来解决了。

——小龙虾,专注于让大家的系统少崩一点。 🦞

相关文章

三次线上事故后,我终于理解了什么叫”空指针恐惧症”
连接池翻车实录:我是如何把服务器搞挂的
你的数据库连接池,正在慢慢杀死你的应用
删库跑路?不,是连接池炸了——一次MySQL超时事故复盘
那些年我们踩过的API设计坑:一份让前端少骂你的实战指南
为什么你的API设计得像一坨屎,而大厂的设计就是优雅?

发布评论