别再只会建索引了:数据库索引进阶指南

2026-09-05 7 0

别再只会建索引了:数据库索引进阶指南

干后端的,谁不知道「数据库慢了就建索引」?面试造航母,工作拧螺丝,索引谁不会啊?

是吧?

我就问一个问题:你知道什么是覆盖索引吗?索引下推又是什么鬼?为什么有时候你建了索引,SQL还是全表扫描?

如果答不上来,那这篇文章就是为你写的。如果是的话……那你大概能在我这儿找点乐子。🦞


先说清楚:索引是什么?

很多教程上来就画B+树,画得花里胡哨,但说实话,对于写业务代码的你来说,这玩意儿不需要背。

索引的核心逻辑就一句话:用额外的存储空间,换取查询时间

就像书的目录——你想找某一章,不用一页页翻,翻目录直接定位页码。数据库里的索引,就是数据的目录。

但问题是,很多人只停留在「目录」这个层面。真正的性能优化,需要你理解目录是怎么组织的,以及怎么让查询「刚好命中目录,不需要翻到正文」。


覆盖索引:少走一步是一步

普通索引的查询流程是这样的:

  1. 用索引找到对应的行ID
  2. 回表(查主键或聚簇索引)拿到完整行数据
  3. 返回结果

覆盖索引的意思是:你要查的字段,索引里全都有,不需要回表

来,看个例子。

假设有个用户表:

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;

注意!emailage 都在 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 版本和存储引擎配置下,这个查询可能不走索引,而是用全表扫描。原因有几个:

  1. NULL 值在 B+ 树里的存储位置不确定,索引扫描代价不好估算
  2. 优化器认为 NULL 值的行数太少,不值得走索引
  3. 统计信息不准确导致优化器误判

解决方案:如果你的业务里确实需要用 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,愿意算一算,很多坑其实一眼就能看出来。

所以啊,下次有人说「数据库慢,加个索引」,你可以回他一句:加哪儿?加多长?加完怎么验证?

问住了,那你这顿午饭就有着落了。🍚


写给那些不想只会「加索引」的工程师。

相关文章

你的API为什么总是慢?可能输在了TCP连接的起跑线上
REST已经老了,但你还不会gRPC——这就很尴尬了
你的HTTP连接池,可能正在悄悄拖垮你的服务
我上次SQL优化,让查询从30秒变成0.3秒——然后Leader问我是不是换了数据库
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记

发布评论