很多后端开发人员写 SQL 时是“面向结果编程”,只要数据出来就行。但上线后面对慢查询日志里的 Query_time: 5.00 时,往往两眼一抹黑。

其实 MySQL 自带了一个免费的“X光机”——EXPLAIN。今天我不讲枯燥的定义,而是带你从执行计划的底层逻辑出发,复盘那些让索引“翻车”的高频场景,以及如何花最小的代价做优化。

一、 读懂 Explain:不是看结果,是看“成本”

很多人只看 type 是不是 ALL,这太片面了。EXPLAIN 的本质是MySQL 优化器基于成本(Cost-Based Optimizer, CBO)的预估。它预估了 CPU 计算量和磁盘 IO 次数。

我把关键字段翻译成“人话”:

  1. id:执行顺序的指挥官
    • id 相同:从上往下执行(先写的先跑)。
    • id 不同:id 越大越先执行(像嵌套循环里的内层循环)。
    • 独特见解: 如果你的 SQL 里有子查询,发现 id 特别大且 rows 很多,说明子查询可能被当成了外层循环,这是性能重灾区。
  2. type:相亲对象的“条件匹配度”
    这是最核心的指标,记住这个梯队(从神到坑):
    • system/const:主键或唯一索引精确查找(只查一行),这是天花板。
    • eq_ref:多表关联时,被驱动表用了主键/唯一索引。
    • ref:非唯一索引查找,能扫到多行,但还算健康。
    • range:范围查找(><between),这里开始有点吃力了。
    • index:全索引扫描(遍历整个索引树),虽然比全表扫描快点,但也是大忌。
    • ALL:全表扫描(Full Table Scan),直接拉出去“埋了”吧。
    • 目标: 至少要优化到 range 级别,最好是 ref
  3. key_len:索引用了多少“料”
    别只看有没有用索引,要看用了多长。
    • 计算逻辑: varchar(10) 用 utf8mb4,长度是 10*4 + 2 = 42 字节。如果 key_len 只有 30,说明只用了前缀索引。
    • 实战意义: 如果是联合索引 (a, b)key_len 只算出了字段 a 的长度,说明 b 没生效(最左前缀失效了)。
  4. Extra:医生的“诊断备注”
    • Using filesort:这是个红牌警告!MySQL 内存不够排序了,必须把数据捞出来在磁盘上排。解决办法:给 ORDER BY 的字段建索引,或者调整 sort_buffer_size
    • Using index:这是绿牌!覆盖索引,数据直接从索引树拿,不用回表,快得飞起。
    • Using where:这是个中性词,说明存储引擎层筛选完后,服务层还要再过滤一遍。如果同时出现 Using where 且 rows 很大,大概率没走好索引。

二、 索引失效的 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万 次查询。

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

四、 总结:优化的心法

  1. 全值匹配:能用 = 就别用 >,能用 > 就别用 LIKE
  2. 最左前缀:联合索引要按查询频率排序,最常用的放左边。
  3. 覆盖索引:这是终极杀器,能不回表就不回表。
  4. 控制范围:单次查询扫描行数(rows)尽量控制在几千行以内,过万就要警惕。

SQL 优化不是玄学,而是数学(成本计算)和数据结构(B+Tree)的博弈。下次遇到慢查询,先别急着加索引,打开 EXPLAIN,看看它到底是怎么“想”的。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐