一条慢 SQL 的执行计划里,最容易被跳过的三行

举报
yd_282927746 发表于 2026/11/09 08:16:40 2026/11/09
【摘要】 type=ref 扫 48 万行也算快?EXPLAIN 里 rows 才是代价;filtered 是乘出来的百分比,key_len 告诉你复合索引用到第几列;Extra 里 Using index 是覆盖索引不是用了索引。附索引失效六个真实现场与深分页三种解法。

一条慢 SQL 的执行计划

线上一条查询慢到 3 秒,拿 EXPLAIN 一看:

+----+-------+------+---------------+---------+---------+----------+----------+-------------+
| id | type  | key  | possible_keys | key_len | ref     | rows     | filtered | Extra       |
+----+-------+------+---------------+---------+---------+----------+----------+-------------+
|  1 | ref   | idx_uid | idx_uid   | 8       | const   | 482193   |  100.00  | Using where |
+----+-------+------+---------------+---------+---------+----------+----------+-------------+

走了索引、type=ref、filtered=100,看起来"没毛病"。但实际上:

  • rows=482193——它要扫 48 万行;
  • Extra 里没有 Using index——意味着每一行都要回表;
  • 没有 Using filesort 也没有 Using temporary——排序和去重走的是索引顺序,这一点倒是没问题。

问题在 rows。走索引不等于走了"好"索引,扫描行数才是代价的直接体现。

EXPLAIN 里有几行参数最容易被跳过,而它们恰好决定了性能:

第一行:type 的等级差

从好到坏(常见值):

system > const > eq_ref > ref > range > index > ALL

几个容易误判的:

  • ref 不等于没问题。ref 只说明用了非唯一索引做等值匹配,实际扫多少行看 rows。上面那条 ref 扫了 48 万行,和全表扫描差不多,只是走了 B+ 树而不是顺序读。
  • index 比 ALL 好不了多少。index 是"扫整个索引树",如果索引包含的列很多,它扫的数据量可能比全表扫描还大。看到 type=index 基本可以判定索引没被有效利用。
  • range 要分清范围大小。WHERE create_time > '2026-01-01' 和 WHERE id BETWEEN 1 AND 10 都是 range,但前者可能扫 90% 的行。还是要看 rows。

一个经验判断:rows 超过表总行数的 20%,优化器就该考虑全表扫描了。如果它还固执地走索引,通常是因为回表代价估计得不准,或者 ORDER BY 恰好能用上这个索引的顺序。

第二行:filtered 是乘出来的

很多人以为 filtered 是"过滤掉多少",其实它是"经过这层条件后还剩多少百分比",而且它和 rows 是相乘关系:

实际参与下一步的行数 ≈ rows × filtered / 100

filtered=100 就意味着"这个索引条件筛完,一个都没筛掉"。上面那条 SQL 的 WHERE uid = ? AND status = 1,如果用 idx_uid,uid 筛完剩 48 万行,status = 1 这层才在 Using where 里做过滤——所以 filtered 是 100 是合理的,因为索引根本没管 status。

这就指向了真正的优化点:把 status 加进索引,变成 idx_uid_status(uid, status)。此时 key_len 会变长(多一个字段),rows 直接降到实际的 status=1 行数。

顺带说 key_len 的读法:它告诉你"索引用了几个字节",可以用来判断复合索引用到了第几列。比如 idx_a_b_c,a 是 int(4 字节,可空再加 1),如果 key_len 只有 4,说明只用到第一列,b 和 c 白建了。

第三行:Extra 里的四个关键词

Extra 字段是信息量最大的地方,重点看这四个:

Extra 含义 要警惕吗
Using where 存储引擎返回行后,Server 层还要再过滤 看 rows 和 filtered,可能要改索引
Using index 覆盖索引,不回表 好事
Using index condition 索引下推(ICP),在引擎层过滤 好事
Using filesort 需要额外排序 看数据量,可能要加排序字段到索引
Using temporary 用了临时表 通常要改(GROUP BY / DISTINCT / UNION)

两个容易被误解的:

Using index 才是"不回表",不是"用了索引"。 只要走了索引,key 列就有值;Using index 专指"查询的所有列都在索引里",不需要回表取数据。它的性能差异是数量级的——回表是随机 IO,覆盖索引是顺序读索引树。

Using filesort 不代表用了磁盘文件。 名字有误导性,它可能全在内存里完成(sort_buffer_size 内)。真正要关注的是排序的数据量:rows 很大 + Using filesort,才需要处理。

索引失效的六个真实场景

这一节的每一条都能用 EXPLAIN 验证,不必背,看到执行计划里 key=NULL 或 type=ALL 就对应上了。

① 列上做了函数或运算

-- 失效
SELECT * FROM orders WHERE DATE(create_time) = '2026-11-09';
-- 有效
SELECT * FROM orders WHERE create_time >= '2026-11-09 00:00:00'
                      AND create_time <  '2026-11-10 00:00:00';

原因是 B+ 树按列的原始值排序,函数值没有顺序,没法二分查找。这也是为什么"按天统计"的查询一定要写成范围区间。

② 隐式类型转换

-- phone 是 varchar,下面这个会发生转换,索引失效
SELECT * FROM users WHERE phone = 13000000000;
-- 正确
SELECT * FROM users WHERE phone = '13000000000';

判断方向很重要:字符串列和数字比较时,MySQL 把字符串转成数字,转换发生在列上,索引失效。反过来,如果列是 int 而参数是字符串 '123',MySQL 把参数转成数字,索引正常。所以"数字列传字符串没问题,字符串列传数字就完蛋"。

③ 前导通配符

WHERE name LIKE '%abc'     -- 失效
WHERE name LIKE 'abc%'     -- 有效

% 在最左边就没法用索引有序性。真实场景里"搜关键词"的需求,正确解法是全文索引(FULLTEXT)或外部搜索引擎,不要指望 LIKE '%...%'。

④ 复合索引的最左前缀断裂

索引 idx_a_b_c(a, b, c):

WHERE a = 1 AND b = 2 AND c = 3   -- 全用上
WHERE a = 1 AND c = 3             -- 只用上 a(c 用不上,因为 b 断了)
WHERE b = 2 AND c = 3             -- 完全用不上
WHERE a = 1 AND b > 2 AND c = 3   -- a、b 用上,c 因为范围查询而失效

最后一条是坑:范围查询后面的列用不上索引(8.0 的索引跳跃扫描 skip scan 有改善,但依赖统计信息,别指望)。

⑤ OR 连接了不同列

WHERE a = 1 OR b = 2

如果 a 和 b 上分别有索引,优化器可能选择 index_merge(type=index_merge),但这种情况性能往往不如预期,很多团队直接禁用它。更常见的处理是拆成两条 SQL 用 UNION,或者建一个包含两列的复合索引(如果业务允许)。

⑥ 排序字段和索引顺序不一致

SELECT * FROM t WHERE a = 1 ORDER BY b DESC, c ASC;

要求索引是 (a, b DESC, c ASC) 或者 (a, b, c) 且 ORDER BY 方向一致(MySQL 8.0 支持降序索引,8.0 之前混合方向一定 filesort)。这种问题 EXPLAIN 里表现为 Using filesort,rows 不大时容易被忽略,等数据量涨上来才暴露。

两个经常被忽略的执行计划细节

细节一:EXPLAIN 不执行 SQL,EXPLAIN ANALYZE 会。

EXPLAIN 给的是优化器的估算,rows 和 filtered 都可能差几个数量级(尤其统计信息没更新时)。MySQL 8.0.18+ 支持:

EXPLAIN ANALYZE SELECT ...;

输出里会给出每个节点的 actual rows 和 actual time,和估算值对照,能立刻看出"哪个环节估错了"。这是排查慢 SQL 最快的手段——估错通常意味着统计信息陈旧,ANALYZE TABLE 一下可能就好了。

细节二:统计信息是会过期的。

information_schema.tables 里有 data_length、index_length,但更关键的是采样。InnoDB 默认 innodb_stats_persistent=ON,每个索引采样 20 个页(innodb_stats_persistent_sample_pages)。大表上 20 个页可能完全代表不了真实分布——这就是"同一条 SQL 昨天 50ms、今天 3 秒"的常见原因之一。

定期 ANALYZE TABLE,或者调大采样页数(代价是采样的时间变长)。

深分页:不是索引问题

SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

这条即使走了主键索引也很慢,因为它要扫 100 万行然后丢掉。EXPLAIN 里看起来一切正常(type=index、rows=1000020),问题在语义上。

三种解法,按适用性排序:

  1. 游标式分页(最推荐):WHERE id > 上次最大 id ORDER BY id LIMIT 20,翻页成本恒定;
  2. 子查询延迟关联:SELECT o.* FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000,20) t USING(id),子查询走覆盖索引,只在最后 20 行回表;
  3. 限制最大页数(产品层面的妥协)。

一条能落地的检查顺序

拿到一条慢 SQL,我会按这个顺序看,不跳步:

  1. EXPLAIN 看 type 和 rows —— 是不是压根没走索引,或者走了但扫得太多;
  2. 看 Extra 有没有 Using filesort / Using temporary —— 有没有额外排序和临时表;
  3. 看 key_len —— 复合索引到底用到了第几列;
  4. 上用 EXPLAIN ANALYZE 对照估算与实际 —— 判断是不是统计信息的问题;
  5. 才动手改索引或改写 SQL。

跳过前三步直接加索引,是常见的浪费时间方式:索引加了一堆,写入变慢、磁盘变大,查询还是那样——因为瓶颈可能根本不在索引上,而在排序的数据量或者回表的随机 IO。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。