约 8 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
没有索引时,数据库只能一行一行读表并逐行判断条件,10 万行就是 10 万次判断。索引是一棵按列值排序的 B+ 树:叶子节点按键值有序并用链表串起来,内部节点只存键。查找时先在树里定位到目标值所在的区间,再沿叶子链表读取相邻行——1000 万行的表和 1000 行的表,定位成本几乎一样。范围查询也能吃到红利,因为区间内的行在叶子链上是物理连续的。代价写在别处:每个索引都是一棵额外的树,写入时必须同步维护,索引越多写越慢、占的空间越大。
EXPLAIN 输出一堆字段,但只有 type、rows 和 Extra 决定性能走向。先看 type 判断访问方式,再看 rows 估算要读多少行,最后看 Extra 有没有 USING INDEX、USING TEMPORARY、USING FILESORT 这三个关键词。只要出现 USING TEMPORARY 且后面跟着 USING FILESORT,排序就已经落到磁盘上了,再大的索引也救不回来。
| type 值 | 含义 | 性能判断 |
|---|---|---|
| const | 主键或唯一索引上的等值查询,最多一行 | 最好,不用优化 |
| eq_ref | 连接时对每行用主键/唯一索引精确匹配一次 | 很好 |
| ref | 等值条件命中普通(非唯一)索引,可能多行 | 可以接受 |
| range | 索引上走了范围:>、<、BETWEEN、IN 的一部分 | 可接受,注意范围后的列用不上索引 |
| index | 扫描整棵索引树(比全表快一点,但仍是全扫) | 偏慢,通常是缺合适的索引 |
| ALL | 全表扫描 | 该建索引,或先缩小过滤范围 |
建了索引 (a, b, c) 之后,只有查询条件从最左列开始连续使用,才能走到这个索引。这就是最左前缀规则。查 a 能用,查 a 和 b 能用,查 b 单独就完全用不上。原因是索引内部先按 a 排序,a 相同再按 b 排序;只给 b 条件时,全表里 b 的值是交错散落的,结构上无法定位。列序安排的经验法则是:等值条件在前、范围条件在后、排序与分组列尽量靠后。
WHERE user_id = 7 → 用得上(索引第 1 列) WHERE user_id = 7 AND created_at > NOW() - INTERVAL 1 DAY → 用得上(第 1、2 列) WHERE user_id = 7 AND status = 1 → 用不上第 3 列,created_at 断了 WHERE created_at > NOW() - INTERVAL 1 DAY → 完全用不上(缺最左列) WHERE status = 1 → 完全用不上
索引 (user_id, created_at) 低效写法:WHERE created_at > ? AND user_id = ? 范围在最左,user_id 用不上索引 高效写法:WHERE user_id = ? AND created_at > ? 两个条件都在索引上
SELECT user_id, status FROM orders WHERE user_id = 7; 索引 (user_id, status) → Extra 出现 USING INDEX,全在索引里读完,不用回表 索引 (user_id) → Extra 出现 Using where,需要回表再取 status 注意:SELECT * 会让覆盖索引失效,因为 * 需要的列不在索引里
一个经验判断:读多写少的表(配置表、字典表、维表)可以按查询模式多建几个索引;写多的表(订单、日志、流水)要克制,因为每插一行,所有索引的 B+ 树都要分裂或合并,还可能触发页分裂和随机 IO。真正需要判断的是「这条索引值不值」——看它的选择性:区分度高的列(几乎不重复,比如订单号、用户 ID)适合建索引,低区分度的列(状态、类型、性别)单独建不如放进联合索引的后半段。
type 看访问方式,从好到差是 const、eq_ref、ref、range、index、ALL,出现 ALL 或 index 基本就是没走上合适的索引;rows 是优化器估算要扫的行数,估算值和真实值差一个数量级说明统计信息过期,先跑 ANALYZE 再看。
按最左前缀规则:等值条件放最前、范围条件放其后、排序或分组列尽量靠后。比如高频查询是 WHERE user_id = ? AND created_at > ?,就建 (user_id, created_at);反过来写会让 created_at 的范围条件把索引断掉。
常见原因有五个:列上套了函数或运算、隐式类型转换(varchar 列比数字)、前导百分号 LIKE '%x'、OR 里有没索引的一侧、以及索引选择性太低(比如性别列)。先用 EXPLAIN 确认 type 是不是 ALL,再逐个排除。
这是深分页,瓶颈不在索引而在丢弃行数:数据库要扫过并扔掉前 10 万行。改成 WHERE id > 上一页最后一个 id ORDER BY id LIMIT 20,用主键当游标,代价与页码无关,总量再大也稳定。