MySQL 性能急救手册:手把你教看懂 Explain,避开索引失效的 10 个“坑”
很多后端开发人员写 SQL 时是“面向结果编程”,只要数据出来就行。但上线后面对慢查询日志里的 Query_time: 5.00 时,往往两眼一抹黑。
其实 MySQL 自带了一个免费的“X光机”——EXPLAIN。今天我不讲枯燥的定义,而是带你从执行计划的底层逻辑出发,复盘那些让索引“翻车”的高频场景,以及如何花最小的代价做优化。
一、 读懂 Explain:不是看结果,是看“成本”
很多人只看 type 是不是 ALL,这太片面了。EXPLAIN 的本质是MySQL 优化器基于成本(Cost-Based Optimizer, CBO)的预估。它预估了 CPU 计算量和磁盘 IO 次数。
我把关键字段翻译成“人话”:
- id:执行顺序的指挥官
id相同:从上往下执行(先写的先跑)。id不同:id 越大越先执行(像嵌套循环里的内层循环)。- 独特见解: 如果你的 SQL 里有子查询,发现
id特别大且rows很多,说明子查询可能被当成了外层循环,这是性能重灾区。
- type:相亲对象的“条件匹配度”
这是最核心的指标,记住这个梯队(从神到坑):- system/const:主键或唯一索引精确查找(只查一行),这是天花板。
- eq_ref:多表关联时,被驱动表用了主键/唯一索引。
- ref:非唯一索引查找,能扫到多行,但还算健康。
- range:范围查找(
>,<,between),这里开始有点吃力了。 - index:全索引扫描(遍历整个索引树),虽然比全表扫描快点,但也是大忌。
- ALL:全表扫描(Full Table Scan),直接拉出去“埋了”吧。
- 目标: 至少要优化到
range级别,最好是ref。
- key_len:索引用了多少“料”
别只看有没有用索引,要看用了多长。- 计算逻辑:
varchar(10)用utf8mb4,长度是10*4 + 2 = 42字节。如果key_len只有 30,说明只用了前缀索引。 - 实战意义: 如果是联合索引
(a, b),key_len只算出了字段a的长度,说明b没生效(最左前缀失效了)。
- 计算逻辑:
- Extra:医生的“诊断备注”
- Using filesort:这是个红牌警告!MySQL 内存不够排序了,必须把数据捞出来在磁盘上排。解决办法:给
ORDER BY的字段建索引,或者调整sort_buffer_size。 - Using index:这是绿牌!覆盖索引,数据直接从索引树拿,不用回表,快得飞起。
- Using where:这是个中性词,说明存储引擎层筛选完后,服务层还要再过滤一遍。如果同时出现
Using where且rows很大,大概率没走好索引。
- Using filesort:这是个红牌警告!MySQL 内存不够排序了,必须把数据捞出来在磁盘上排。解决办法:给
二、 索引失效的 10 种“死法”:你中招了吗?
索引不是建了就能用,以下这些操作相当于给索引“下毒”:
1. 在索引列上做“手术”
场景:WHERE DATE(create_time) = '2024-01-01'
原理:B+Tree 存的是 create_time 的原始值,你用函数一包装,数据库就不认识了,只能全表扫描。
解法:改成范围查询 WHERE create_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59'。
2. 模糊查询的“左截断”
场景:WHERE name LIKE '%张三'
原理:B+Tree 是有序的,像字典一样从左到右排序。你把左边砍了(% 在前),字典就没法二分查找了。
解法:如果必须用,考虑用 ElasticSearch 做全文索引,或者用 LOCATE 函数(但性能也一般)。
3. 隐形的“类型转换”
场景:phone 是 varchar,你写 WHERE phone = 13800138000(没加引号)。
原理:MySQL 会把全表的 varchar 转成 int 再比较,这一转,索引就废了。
解法:严格保持类型一致,字符串加引号!
4. 最左前缀的“背叛”
场景:联合索引 (a, b, c),你查 WHERE b=2 AND c=3。
原理:跳过了 a,相当于从联合索引的第二页开始找,索引结构是不连续的。
解法:如果业务经常跳过 a,考虑建 (b, c) 的索引,或者强制带上 a 的查询条件。
5. 范围查询后的“断层”
场景:联合索引 (a, b, c),你查 WHERE a=1 AND b>10 AND c=2。
原理:b 用了范围查询,b 后面的 c 在 b 不确定的情况下也是无序的,所以 c 的索引失效。
独特见解:这叫“区间挡板”效应,一旦遇到范围查询(>, <),后面的联合索引字段全部失效。
6. 排序方向的“逆反”
场景:索引是 (a ASC, b ASC),你写 ORDER BY a ASC, b DESC。
原理:MySQL 8.0 之前不支持降序索引,反向排序需要额外的文件排序(Filesort)。
解法:升级 MySQL 8.0(支持降序索引)或者调整排序字段顺序。
(其余如 !=、IS NOT NULL、无过滤条件的排序等,本质都是因为优化器认为“全表扫描比走索引再回表更便宜”,所以主动放弃了索引。)
三、 高阶优化:不仅仅是建索引
当单表优化到极致,我们要看多表交互和排序。
1. JOIN 的艺术:小表驱动大表
- Inner Join:MySQL 优化器会自动选小表做驱动表(外层)。
- Left Join:左表是驱动表。铁律:给被驱动表(右表)的关联字段建索引!
- 比喻:就像循环嵌套,外层循环次数越少越好。如果左表有 100 行,右表有 100 万行,左表连右表时,右表的关联字段必须有索引,否则就是
100 * 100万次查询。
- 比喻:就像循环嵌套,外层循环次数越少越好。如果左表有 100 行,右表有 100 万行,左表连右表时,右表的关联字段必须有索引,否则就是
2. 子查询 vs JOIN
老观念:子查询慢。
新观念:MySQL 5.6+ 对子查询做了很多优化(Semi-Join),但依然推荐用 JOIN。
- NOT IN / NOT EXISTS:这俩是性能黑洞,尤其是子查询里有大量数据时。
- 优化神技:用
LEFT JOIN ... WHERE right.id IS NULL替代NOT IN。- 原理:
NOT IN一旦遇到 NULL 会全表扫描;而LEFT JOIN是明确的连接操作,优化器能更好地利用索引。
- 原理:
3. 排序与分组的“内存战”
MySQL 的排序有两种模式:
- 单路排序(Sort Row):一次性把
SELECT的字段全取出来排序。快,但占内存。如果数据太大(超过max_length_for_sort_data),会频繁读写磁盘。 - 双路排序(Sort Key + Row ID):只取排序字段和主键,排好序再回表取数据。IO 多,但内存占用小。
调优建议: - 如果查询只需要索引覆盖的字段,直接走索引顺序,避免
filesort。 - 适当调大
sort_buffer_size(但别太大,这是每个连接独占的内存,容易撑爆服务器)。 GROUP BY实质上也是一种排序,优化原则同ORDER BY。
四、 总结:优化的心法
- 全值匹配:能用
=就别用>,能用>就别用LIKE。 - 最左前缀:联合索引要按查询频率排序,最常用的放左边。
- 覆盖索引:这是终极杀器,能不回表就不回表。
- 控制范围:单次查询扫描行数(
rows)尽量控制在几千行以内,过万就要警惕。
SQL 优化不是玄学,而是数学(成本计算)和数据结构(B+Tree)的博弈。下次遇到慢查询,先别急着加索引,打开 EXPLAIN,看看它到底是怎么“想”的。
更多推荐

所有评论(0)