别再只会建索引了:数据库索引进阶指南
干后端的,谁不知道「数据库慢了就建索引」?面试造航母,工作拧螺丝,索引谁不会啊?
是吧?
我就问一个问题:你知道什么是覆盖索引吗?索引下推又是什么鬼?为什么有时候你建了索引,SQL还是全表扫描?
如果答不上来,那这篇文章就是为你写的。如果是的话……那你大概能在我这儿找点乐子。🦞
先说清楚:索引是什么?
很多教程上来就画B+树,画得花里胡哨,但说实话,对于写业务代码的你来说,这玩意儿不需要背。
索引的核心逻辑就一句话:用额外的存储空间,换取查询时间。
就像书的目录——你想找某一章,不用一页页翻,翻目录直接定位页码。数据库里的索引,就是数据的目录。
但问题是,很多人只停留在「目录」这个层面。真正的性能优化,需要你理解目录是怎么组织的,以及怎么让查询「刚好命中目录,不需要翻到正文」。
覆盖索引:少走一步是一步
普通索引的查询流程是这样的:
- 用索引找到对应的行ID
- 回表(查主键或聚簇索引)拿到完整行数据
- 返回结果
覆盖索引的意思是:你要查的字段,索引里全都有,不需要回表。
来,看个例子。
假设有个用户表:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
age INT,
created_at DATETIME,
INDEX idx_age_email (age, email)
);
现在执行这个查询:
SELECT email FROM users WHERE age = 25;
注意!email 和 age 都在 idx_age_email 这个索引里定义过了。数据库直接通过索引就拿到了 email,不需要再回表查主键。
这就是覆盖索引,查询直接从索引树返回数据,零回表。Explain 里的 type 会显示 ref,Extra 里你会看到 Using index——这是好现象。
怎么判断自己有没有用上覆盖索引?
EXPLAIN SELECT email FROM users WHERE age = 25;
看 Extra 列,有 Using index 就对了。没有,就说明还在回表。
最左前缀原则:顺序不对,努力白费
这条原则,但凡学过数据库的都知道。但知道归知道,实战中踩坑的多了去了。
假设你建了这样一个索引:
INDEX idx_name_age_created (name, age, created_at)
那么:
WHERE name = '张三'—— ✅ 走索引WHERE name = '张三' AND age = 30—— ✅ 走索引WHERE name = '张三' AND age = 30 AND created_at > '2026-01-01'—— ✅ 走索引WHERE age = 30—— ❌ 不走索引(没有name)WHERE name = '张三' AND created_at > '2026-01-01'—— ⚠️ 只走 name 部分,created_at 过滤失效
第三种情况特别容易踩坑:索引是 (name, age, created_at),你只用了 name 和 created_at。数据库会用 name 走索引,但 created_at 的条件只能在索引树扫描的过程中逐条过滤——这叫「索引扫描」而不是「索引查找」,效率差很多。
所以设计复合索引的时候,把等值查询的字段放前面,范围查询的字段放后面。这是铁律。
索引下推:5.6版本以后的隐藏大招
MySQL 5.6 引入了索引下推(Index Condition Pushdown,ICP),很多人不知道这个优化,但它的效果是真的香。
先说没有ICP的时候,查询是怎么跑的:
假设 WHERE name = '张三' AND age > 20,索引是 (name, age)。
没有ICP:数据库先用 name 找到所有张三的记录(可能有很多条),然后逐条回表,在server层过滤 age > 20 的条件。每一条不满足的记录,都多了一次无意义的回表。
有ICP:数据库在索引遍历的过程中,直接在存储引擎层就把 age > 20 的条件用上了。只回表查那些真正满足条件的记录。
用人话来说就是:能提前过滤的,就别等到最后再过滤,别浪费回表次数。
这个优化是自动的,但你可以通过 EXPLAIN 的 Extra 列来验证它有没有生效:看到 Using index condition 而不是 Using where,说明ICP在工作。
NULL值和索引:你可能没想到的坑
NULL 值在索引里的行为,比很多人想象的复杂。
首先,MySQL 里 NULL 并不等于 '' 或者 0。NULL 就是「不知道」,它是特殊的。
索引是支持存储 NULL 值的,比如:
INDEX idx_phone (phone)
这条索引里,phone 为 NULL 的记录也会被索引。但问题是:
如果你写了这样的查询:
SELECT * FROM users WHERE phone IS NULL;
在某些 MySQL 版本和存储引擎配置下,这个查询可能不走索引,而是用全表扫描。原因有几个:
- NULL 值在 B+ 树里的存储位置不确定,索引扫描代价不好估算
- 优化器认为 NULL 值的行数太少,不值得走索引
- 统计信息不准确导致优化器误判
解决方案:如果你的业务里确实需要用 IS NULL 查询,给这类查询单独建个函数索引或者用 COALESCE 配合复合索引。
另外有个反直觉的点:WHERE phone = '' 和 WHERE phone IS NULL 是完全不同的执行计划。前者走索引的可能性远大于后者。
字符串索引:前缀索引的正确姿势
有时候你想给一个很长的 VARCHAR 字段建索引,比如 URL 或者邮箱。直接建全字段索引太占空间,怎么办?
用前缀索引:
ALTER TABLE articles ADD INDEX idx_url (url(10));
这个 url(10) 表示只索引前10个字符。
但这里有个巨大的坑:前缀索引不支持覆盖索引扫描。因为你要的完整URL不在索引里,数据库必须回表拿完整的url值。所以前缀索引只能用于普通查找,不能用于覆盖索引优化。
选择前缀长度也有讲究:怎么选?
一个经验公式是:选择能让索引区分度达到全字段区分度90%以上的前缀长度。怎么算?
SELECT
COUNT(DISTINCT LEFT(url, 5)) / COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(url, 10)) / COUNT(*) AS sel10,
COUNT(DISTINCT LEFT(url, 15)) / COUNT(*) AS sel15,
COUNT(DISTINCT LEFT(url, 20)) / COUNT(*) AS sel20
FROM articles;
挑一个接近全字段区分度的最小前缀长度。低于0.9的话,这个字段本身就不太适合建索引。
对于邮箱字段,前缀长度 6-8 个字符通常就够了,因为用户名部分通常有一定的随机性。但如果是中文姓名字段,前缀索引基本没用——两个字重复率太高。
ORDER BY 和索引:排好序才能用上索引
JOIN 和 WHERE 能走索引大家都知道了,ORDER BY 也能走索引很多人就不知道了。
假设你有这样一个查询:
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
如果 INDEX idx_user_created (user_id, created_at) 存在,这个 ORDER BY 是不需要额外排序的。因为索引本身就是按 (user_id, created_at) 有序排列的,遍历索引就等于排好序的结果。
如果 ORDER BY 字段和索引顺序不一致,比如:
SELECT * FROM orders WHERE user_id = 100 ORDER BY amount DESC;
amount 不在索引里,数据库只能在拿到结果后单独排序。这就是 Using filesort,在大数据量下很慢。
怎么判断?
EXPLAIN SELECT ... ORDER BY created_at DESC;
Extra 列里有 Using index 就可以放心了,有 Using filesort 就得优化。
总结:索引优化的检查清单
来,上货。写SQL或者Review别人代码的时候,按这个清单过一遍:
- 查询是否覆盖索引?Extra 列有没有
Using index? - 复合索引顺序对不对?等值在前,范围在后。
- 有没有违反最左前缀?跳过了前面的字段没有?
- 有没有多余的回表?能用覆盖索引解决吗?
- NULL值查询有没有走到索引?IS NULL 容易踩坑。
- ORDER BY 能不能用索引排序?
Using filesort是红色警报。 - 前缀索引长度选对了吗?区分度够不够?
索引这东西,说难也难,说简单也简单。难在它需要你对数据分布、业务查询、数据库内部机制都有了解;简单在只要你愿意看 EXPLAIN,愿意算一算,很多坑其实一眼就能看出来。
所以啊,下次有人说「数据库慢,加个索引」,你可以回他一句:加哪儿?加多长?加完怎么验证?
问住了,那你这顿午饭就有着落了。🍚
写给那些不想只会「加索引」的工程师。