EXPLAIN 里显示用了索引,不代表索引生效了

EXPLAIN 里显示用了索引,不代表索引生效了

100 万行订单表,InnoDB,数据 248.8 MB。下面每个数字都是实测(取三次最好成绩)。

先看索引值多少钱

makefile 复制代码
建 4 个二级索引耗时 7.9 s
索引体积: 0 MB -> 97.2 MB    (数据本身 248.8 MB)

索引占了数据体积的 39%。 这是买来的东西的价格,下面看它值不值。

有无索引的差距

ini 复制代码
                        无索引                    有索引
按 uid 精确查         433.7 ms  type=ALL        0.3 ms  type=ref    扫描 2 行
按 created 排序取前10  401.1 ms  filesort        0.2 ms  type=index  扫描 10 行

1446 倍 和 2000 倍。这部分没什么悬念。

但同一组测试里有一行很值得说:

lua 复制代码
按 status+city 查 LIMIT 20    无索引 0.8 ms  ← ?

全表扫描,却只要 0.8 毫秒。

因为 status=2 AND city='Hangzhou' 匹配了 2.5 万行,密度极高,全表扫描跑了几千行就凑齐 LIMIT 20 然后提前退出了。

LIMIT + 高频值会让全表扫描看起来无害。 等某天换成一个低频条件------或者用户翻到第 500 页------它立刻变回 400 毫秒。

压测时用的测试数据分布,往往就是这么骗过所有人的。

覆盖索引:少回一次表,快一倍

索引 idx_sc = (status, city, amount),查同样 25038 行:

sql 复制代码
SELECT status,city,amount ...        120.2 ms   Extra: Using index      ← 覆盖
SELECT status,city,amount,payload ... 235.7 ms   Extra: Using index condition ← 回表

只多取了一个 payload 字段,慢了一倍。

因为二级索引的叶子节点只存索引列 + 主键。要拿 payload,就得拿着主键回聚簇索引再查一次------25038 行就是 25038 次回表,而且这些主键是乱序的,对应的是随机 IO。

Extra 里的 Using index 就是「覆盖索引,不用回表」的标志。看到它,说明这个查询已经很便宜了。

实践含义很直接:别写 SELECT * 。不是为了省网络带宽,是为了让索引能覆盖。

最左前缀:key 不是 NULL,不等于索引生效

这是我认为最值得写出来的一组:

perl 复制代码
条件                     耗时      type      key       优化器估算扫描行数
用 status (最左列)       41.7 ms   ref       idx_sc    414,460
用 status + city          6.5 ms   ref       idx_sc     52,634
只用 city (跳过最左)     197.0 ms   index     idx_sc    984,250   ← 注意
只用 amount (第三列)     179.1 ms   index     idx_sc    984,250   ← 注意

后两行的 key 栏明明写着 idx_sc。很多人看到这里就放心了:「索引用上了。」

但 type=index 的含义是 全索引扫描 ------把整棵索引树从头到尾撸一遍,98 万行一行不落,只是恰好不用回表而已。它比全表扫描快一点(因为索引比数据小),但和 type=ref 的 6.5 毫秒差了 30 倍。

看 EXPLAIN 要先看 type,不是 key。 记住这个由好到坏的顺序:

sql 复制代码
system > const > eq_ref > ref > range > index > ALL
                                   ↑ 分界线

range 以上算正常,index 和 ALL 都意味着全量扫描。

索引失效的三种姿势,明码标价

ini 复制代码
写法                          耗时      type     相对正常写法
phone='13812345678'          0.2 ms    ref      基准
phone=13812345678  ← 数字   213.5 ms   index    慢 1068 倍
LIKE '138123%'               0.6 ms    range    正常
LIKE '%2345678'            235.1 ms    index    慢 392 倍
uid = 12345                  0.2 ms    ref      基准
uid+0 = 12345              147.2 ms    index    慢 736 倍

第二行是最阴的一个。 phone 是 VARCHAR,你传了个数字。

SQL 不会报错,结果也完全正确。但 MySQL 的类型转换规则是:字符串和数字比较时,把字符串转成数字 。于是它必须对每一行执行 CAST(phone AS DOUBLE) 再比较------索引树的有序性建立在字符串排序上,转换之后完全用不上了。

慢 1068 倍,零报错,零告警。 ORM 里参数类型写错、JSON 里 "13812345678" 变成 13812345678,都能触发。

第三条同理:只要在索引列上套了函数或做了运算(uid+0、DATE(created)、UPPER(name)),索引就废了。改写方式是把运算挪到常量侧:

sql

sql 复制代码
WHERE created >= 1700000000 AND created < 1700086400   -- ✅
WHERE DATE(created) = '2026-09-23'                      -- ❌

索引不是免费的

bash 复制代码
写入吞吐    有 4 个二级索引    45,132 行/s
           无二级索引        66,948 行/s

每插一行,4 个索引都要各自维护一次 B+ 树。吞吐掉了 33%。

再加上前面那 97.2 MB 的体积------它会一直占着 buffer pool,挤掉本该缓存热数据的空间。

所以「把可能用到的列都建上索引」是错的。没有查询在用的索引,是纯粹的负债。

MySQL 8 可以先用 ALTER TABLE ... ALTER INDEX xxx INVISIBLE 让索引对优化器隐身,观察几天没问题再删------比直接 DROP 安全得多。

一份自查清单

  1. 看 type,不看 key。 index 和 ALL 都是全扫描。
  2. Extra 里出现 Using index 是好事(覆盖索引),出现 Using filesort / Using temporary 要警惕。
  3. 别 SELECT *。 它让覆盖索引失效。
  4. 检查参数类型。 字符串列一定要传字符串。
  5. 联合索引按「等值在前、范围在后、区分度高的在前」排列,并确认查询用到了最左列。
  6. LIKE 只有前缀能用索引。 需要中缀搜索就上全文索引或专门的搜索引擎。
  7. 定期清理没人用的索引。 sys.schema_unused_indexes 能直接告诉你哪些是负债。

最后

索引的直觉很容易停留在「建了就快」。但这次实测里,有索引却慢 200 毫秒的情况出现了四次 ,而每一次 EXPLAIN 的 key 栏都写着索引的名字。

索引存在,和索引生效,是两件事。

相关推荐
苍何2 小时前
开源微信流,微信聊天记录,可以直接给 Codex 和 Obsidan 了
后端
明月_清风2 小时前
JEV 又有新玩法:当 AI Agent 开始拥有一个“决策层”
人工智能·后端·agent
沙蒿同学2 小时前
Wails v2 实战:用 Go + Vue3 做一个真正能用的 AI 桌面应用
前端·javascript·后端
站大爷IP2 小时前
Python的FastAPI把我坑惨了,原来async def和def的区别这么大
后端
货拉拉技术2 小时前
大模型在货拉拉营销广告的应用实践
后端
怕浪猫2 小时前
AI 知识库 WeKnora(腾讯微信团队出品)
后端·面试·github
Gopher_HBo2 小时前
zap日志 整体架构与数据流
后端
颜进强2 小时前
09 · NestJS Middleware 中间件:链路最外层那个"最像 Express"的家伙
前端·后端·ai编程
创新技术阁2 小时前
FastapiAdmin 实战:演示模式开关失效的排查记录
前端·后端·fastapi