线上 SQL 慢到报警,打开慢查询日志一看,几乎都是同一个原因:查询没有走索引,在扫全表。索引优化不是背概念,而是要能回答三个问题:索引底层怎么组织、为什么有的查询用不上索引、EXPLAIN 到底怎么看。这篇用真实场景串起来。
一、先理解 B+ 树:为什么索引能加速
InnoDB 的索引底层是 B+ 树,几个关键特性:
- 矮胖:每个节点能存大量子节点(高扇出),三层树就能覆盖千万级数据,查询时磁盘 IO 次数极少(树高就是 IO 次数);
- 叶子节点存数据并双向链表相连:所以范围查询(BETWEEN、大于小于)只需要顺着叶子链表走,非常高效;
- 数据有序存储:等值、范围、排序都能利用这个有序性。
理解“有序”是理解索引的钥匙:为什么
WHERE name LIKE '张%' 能用索引而 LIKE '%张' 不能?因为前者匹配的是前缀,B+ 树里数据按前缀有序;后者要匹配后缀,顺序帮不上忙。二、聚簇索引与二级索引:回表是怎么回事
| 类型 | 叶子节点内容 | 说明 |
|---|---|---|
| 聚簇索引(主键) | 完整行数据 | InnoDB 每张表必须有一个,按主键组织 |
| 二级索引(普通索引) | 索引列值 + 主键值 | 通过二级索引查到主键,再回聚簇索引取整行 |
所以 SELECT * 走二级索引后通常要回表(多一次主键查找);如果查询的列恰好都在索引里(覆盖索引),就不用回表,EXPLAIN 里表现为 Using index——这是让查询再快一档的常用手段。
三、联合索引:最左前缀原则
联合索引 (user_id, status, create_time) 的查找顺序是先按 user_id、再按 status、再按 create_time,相当于建了三棵“前缀有序”的树,能匹配的条件组合是:
-- ✅ 能用(最左前缀)
WHERE user_id = 1
WHERE user_id = 1 AND status = 2
WHERE user_id = 1 AND status = 2 AND create_time > '2024-01-01'
-- ❌ 用不上(跳过了最左列)
WHERE status = 2 AND create_time > '2024-01-01'
联合索引里把等值列放前面、范围列放后面是基本纪律:范围条件(>、<、BETWEEN)之后的列无法继续用索引过滤,写 EXPLAIN 前先按这个顺序自查。
四、索引为什么失效:最常见的六个坑
- 对索引列做函数或运算:
WHERE YEAR(create_time) = 2024会让索引失效,改为create_time BETWEEN '2024-01-01' AND '2024-12-31'; - 隐式类型转换:列是字符串却用数字比较(或反之),MySQL 会转类型导致无法走索引;
- 前导模糊查询:
LIKE '%keyword'; - OR 连接未全部有索引:
a = 1 OR b = 2,若 b 无索引则整体难走索引,可拆 UNION; - 索引区分度太低:列只有两三个取值(如 status),优化器认为扫全表更快,会主动放弃索引;
- 排序/分组字段组合不匹配索引顺序:出现 Using filesort / Using temporary。
五、EXPLAIN 实战:每列看什么
EXPLAIN SELECT * FROM t_order
WHERE user_id = 100 AND status = 1 ORDER BY create_time DESC LIMIT 20;
-- 重点看这几列:
-- type : 从好到坏 system > const > eq_ref > ref > range > index > ALL
-- key : 实际用到的索引,NULL 说明没走索引
-- rows : 预估扫描行数,数量级越接近实际结果越好
-- Extra : Using index(覆盖索引)、Using filesort(需优化排序)、Using where
排查套路:先把慢 SQL 丢进 EXPLAIN → type 是 ALL 或 key 是 NULL → 查是不是命中上面的“六坑” → 改正后重看 EXPLAIN,rows 明显下降才算完。
六、建索引的正确姿势
- 索引是“加给查询的”:拿业务里真实慢 SQL 来加,而不是拍脑袋给每列都建;
- 控制数量:索引占空间且拖慢写入,一张表一般建议不超过五六个,经常一起查的列合并成联合索引;
- 短列优先:优先用长度短、区分度高的列;
- 上线前检查:用
EXPLAIN验证后发布,大表加索引注意锁表窗口,尽量低峰期执行。
七、慢查询日志:先定位再优化
-- 开启慢查询日志(生产建议同时开启,阈值设 1 秒)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
-- 事后分析
mysqldumpslow -s t /var/log/mysql/mysql-slow.log # 按时间排序看最慢的
# 或 pt-query-digest 汇总相同指纹的 SQL,按总耗时排
慢查询日志的价值是让优化有靶子:别等用户投诉,让日志告诉我们哪几条 SQL 最该先优化,改一条、EXPLAIN 一次、看曲线下降,重复这个过程,数据库就稳了。