索引并非建得越多越好,关键在于让每一次查询都尽可能少地与磁盘交互。下面三个优化技巧,都是从减少回表次数或缩小索引体积的角度出发,在实际开发中性价比极高。
12.4.1 覆盖索引:让查询在索引树上完成,避免回表
回表是指通过二级索引查到主键后,还要再到聚簇索引中读取完整行数据。如果查询所需的所有列都包含在索引中,那么数据库直接在索引树上就能返回结果,不需要回表,这就是覆盖索引。
覆盖索引的核心价值在于:将“索引扫描+回表”两次操作压缩为“索引扫描”一次操作。尤其在数据量大、回表次数多时,性能差距可达数倍甚至数十倍。
真实场景:
一个用户表 users(id, name, age, mobile, email),对 age 建了普通索引。若执行:
SELECT id, age FROM users WHERE age > 25;
age 索引的叶子节点存储的是 (age, id),查询需要的 id 和 age 都在索引里,可以直接返回,这就是覆盖索引。反之,若需要 mobile 字段,索引里没有,就必须拿着 id 回表。
如何设计覆盖索引:
- 不要只为
WHERE条件建索引,还要考虑SELECT中的列,让它们也进入索引树。 - 常用技巧是建联合索引,例如对
(age, mobile)建索引,查询SELECT id, age, mobile FROM users WHERE age > 25就能完全覆盖。 - 注意:
SELECT *几乎不可能利用覆盖索引,建议只查真正需要的字段。
几个值得注意的点:
- 覆盖索引只对当前索引能覆盖的列有效,多余字段仍需回表。
- 太多的列放在索引里会增大索引体积,影响写入性能,平衡点在于:覆盖索引通常用于高频关键查询。
EXPLAIN中Extra列出现Using index(而非Using index condition),代表完全覆盖且不需要回表。
12.4.2 索引下推:让引擎层提前过滤,减少回表
在某些查询中,尽管索引不能完全覆盖 SELECT 列,但可以在索引扫描过程中提前过滤掉不满足条件的记录,从而减少后续的回表次数。这个能力在 MySQL 5.6 中引入,称为索引下推(Index Condition Pushdown, ICP)。
原理对比:
假设有联合索引 idx_name_age (name, age),执行:
SELECT * FROM users WHERE name LIKE '张%' AND age = 20;
- 无 ICP 时:存储引擎通过索引找到所有
name LIKE '张%'的记录,每一条都回表取完整行,再交给 Server 层判断age = 20。即使很多记录的age不满足,也白白回表了。 - 有 ICP 时:存储引擎在遍历索引时,直接检查索引中的
age字段是否等于 20,只将满足条件的记录回表。因为age就在联合索引里,不需要读完整行就能判断。这就大幅减少了不必要的回表。
实际效果: 在 name 过滤后仍然有大量不符合 age 条件的场景,ICP 带来的性能提升非常明显。如果索引中已包含过滤字段,ICP 可以减少大量随机 I/O。
查看方式: EXPLAIN 中 Extra 显示 Using index condition 即表示使用了索引下推。
注意: 索引下推不仅适用于范围查询,很多情况下优化器会自动选择是否启用,开发者只需理解其原理,并在设计索引时将经常联合过滤的字段放在合适的索引列中,就能自然享受到该优化。
12.4.3 前缀索引:用更小的索引体积优化长字符串
当需要对很长的字符串列(如 VARCHAR(255) 的邮箱、地址、长文本摘要)建索引时,完整索引体积会非常大,拖慢写入并占用大量内存。前缀索引只对字符串的前几个字符建立索引,大幅节省空间。
语法:
CREATE INDEX idx_email ON users(email(15)); -- 对email的前15个字符建索引
适用场景:
- 字符串较长但前几个字符区分度已经足够高。比如
email字段,前 15 个字符大概率能区分绝大多数用户。 - URL、长文本开头等。
前缀长度的选择非常关键: 太短则区分度差,索引过滤出的数据仍然很多;太长则节约效果不明显。核心指标是前缀的选择性(索引中不重复的索引值数与数据表总记录数的比率)。通常要使区分度接近完整列的区分度。
计算方法:
-- 先计算完整列的区分度
SELECT COUNT(DISTINCT email) / COUNT(*) FROM users;
-- 再计算不同前缀长度的区分度
SELECT COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) FROM users;
SELECT COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) FROM users;
-- 选择接近完整区分度且长度较短的方案
前缀索引的代价和限制:
- 无法使用前缀索引做
ORDER BY和覆盖索引,因为索引中只存了前缀,无法精确排序,也无法回表后补充剩余字符。 - 对隐式排序的
DISTINCT和GROUP BY也有影响。 - 在
WHERE条件中使用前缀索引时,优化器会通过前缀定位到记录,但仍需要回表读取完整值并比对(即二次判断),因为前缀相同不代表整列相同。
因此,前缀索引是一种空间换时间失败后反向操作的技巧:当整个字段无法全量索引或索引体积成为瓶颈时,它是最优解。对于区分度高、长度适中的列,尽量不要前缀,直接用完整索引;对于超长且前缀区分度够的场景,前缀索引是务实的方案。
这三个技巧都指向同一个目标:让索引在更少的磁盘 I/O 下返回正确的结果。覆盖索引消灭回表,索引下推减少回表次数,前缀索引降低索引体积让索引更高效。日常 SQL 优化中,观察 Extra 列、分析是否回表,是突破性能瓶颈的快捷路径。