Skip to content

为什么索引建了,查询还是慢?—— 索引与 SQL 优化 ​

属于 S1 MySQL 深入 · 深入篇第三篇 上一篇:事务与 MVCC 下一篇:慢查询优化实战

很多人的直觉是"慢查询?加个索引就好了"。但现实里常遇到:索引明明建了,查询还是走全表扫描。问题在于——索引能不能生效,取决于你的 SQL 写法有没有踩中它的规则。这一篇就讲清:索引长什么样、怎么才能用上它、以及怎么排查慢 SQL。

索引有两层:聚簇索引和二级索引 ​

一张 InnoDB 表有两类索引。聚簇索引就是主键索引,它的叶子节点直接存整行数据,一张表只有一个。二级索引是你建的普通索引,它的叶子节点只存"索引列的值 + 主键值"。

这就引出一个关键概念——回表:通过二级索引查到主键,还要回到聚簇索引把整行数据捞出来,多一次 IO。比如你给 name 建了索引,SELECT * FROM t WHERE name='张三' 会先在 name 索引里找到主键 id,再回表取整行。回表是有成本的,能省就省。

怎么省?覆盖索引——让查询要的字段全在索引里。比如 SELECT name, age FROM t WHERE name='张三',如果你有 (name, age) 联合索引,name 和 age 都在索引叶子节点里,就不需要回表了,EXPLAIN 里会显示 Using index,这是最理想的情况。

联合索引和最左前缀 ​

多个列组成的索引叫联合索引。它有个"最左前缀"规则,理解它得先理解 B+ 树的排序方式:联合索引 (a, b, c) 是先按 a 排,a 相同再按 b,再按 c。所以:

text
(a)        ✓ 能走索引
(a, b)     ✓ 能走
(a, b, c)  ✓ 能走
(b, c)     ✗ 走不了(跳过了最左的 a)
(b)        ✗ 走不了

本质是:B+ 树先按 a 有序,你跳过 a 直接查 b,索引没法帮你二分定位。同理,如果中间有范围查询,右边的列也失效了——a=1 AND b>5 AND c=3 里,a、b 能走索引,c 走不了,因为 b 已经是范围了,c 没法继续有序使用。所以设计联合索引的口诀是:等值列放前面,范围列放最后。

索引失效的常见坑 ​

除了最左前缀,还有几个高频失效场景,都是"破坏了索引列的有序性":

  • 对索引列用函数:WHERE YEAR(create_time)=2024,列被函数一包,索引没法直接比较。
  • 隐式类型转换:phone 是 varchar,写 WHERE phone=13800138000(数字),字符串被转成数字比较,走全表。
  • 左模糊查询:LIKE '%张' 走不了索引,LIKE '张%' 能走——因为前缀匹配才能用索引定位起点。
  • OR 连接非索引列、负向查询(!=、NOT IN)通常也失效。

这些坑的共同点:让优化器没法利用索引的"有序"去定位。

慢 SQL 怎么排查:EXPLAIN ​

排查慢 SQL 的标准流程是:慢查询日志定位到具体语句 → EXPLAIN 看执行计划 → 对症优化。

EXPLAIN SELECT ... 会输出一张表,几个关键列:

  • type:访问类型,从优到劣是 const(主键等值)> eq_ref > ref(普通索引等值)> range(范围)> index(全索引扫)> ALL(全表扫,最差)。
  • key:实际用到的索引,NULL 表示没走索引。
  • rows:预估扫描行数,越大越慢。
  • Extra:Using index(覆盖索引,最优)、Using filesort(额外排序,通常要优化)、Using temporary(用了临时表)。

看到 type=ALL 且 rows 很大,基本就是没走索引,按前面的规则找原因;看到 Using filesort,考虑把排序列加进索引。

几个常见优化手段:加合适索引(覆盖、联合最左匹配)、改写 SQL 避开失效场景、大表深分页(LIMIT 100000, 10)用"延迟关联"——先只查主键,再回表取数据。


串起来 ​

索引不是"建了就一定快",它能不能生效取决于 SQL 是否踩中"最左前缀、不破坏有序性"这些规则。排查时用 EXPLAIN 看 type/key/Extra,type=ALL 和 Using filesort 是重点信号。这一套理解下来,面对慢查询就有了"从现象到根因"的路径,而不是盲目加索引。

下一篇讲慢查询优化实战:线上慢 SQL 到底怎么发现、怎么用 EXPLAIN 全字段定位根因、怎么对症优化并验证?

持续学习,持续构建。