华为云RDS慢查询治理与索引失效定位

举报
yd_221506059 发表于 2026/10/16 11:28:46 2026/10/16
【摘要】 慢日志里最扎眼的那条 SQL 往往不是最该优化的。本文先给出挑选标准:按平均耗时乘执行次数排序——高频的 48 毫秒查询一小时能吃掉 576 秒,却不会出现在慢日志里。接着列出索引失效的六种写法,并解释加了索引反而更慢的四个原因:优化器选错索引、回表代价超过全表扫描、选择性太低、索引太多拖垮写入。文末给出覆盖索引与延迟关联的手法,以及必须同时看写入指标的验证口径。

华为云 RDS 慢查询治理:为什么加了索引反而更慢

“这条 SQL 慢,加个索引吧”——这句话在生产库里翻车的概率不低。这篇文章讲清楚三件事:怎么从几百条慢 SQL 里挑出真正该优化的那几条、索引失效的六种典型写法、以及为什么有些索引加上去之后查询反而更慢。

一、先别急着加索引,先学会挑 SQL

慢日志里通常躺着几百条 SQL,全优化一遍是不可能的。挑选标准不是"哪条最慢",而是"哪条总耗时最高":

优化价值 = 平均执行耗时 × 执行次数

举两个真实的对比:

SQL 平均耗时 每小时次数 每小时总耗时
A(报表统计) 3200 ms 4 12.8 s
B(列表查询) 48 ms 12000 576 s

单看慢日志,A 每次都超阈值,最扎眼;B 因为没超过 1 秒的慢查询阈值,根本不会出现在慢日志里,但它一小时吃掉的时间是 A 的 45 倍。

所以第一步是查 performance_schema 的汇总表,或者直接用华为云 DAS 的 Top SQL / SQL 诊断,按总耗时排序:

SELECT
    DIGEST_TEXT,
    COUNT_STAR                         AS exec_count,
    ROUND(SUM_TIMER_WAIT / 1e12, 2)    AS total_sec,
    ROUND(AVG_TIMER_WAIT / 1e9, 2)     AS avg_ms,
    SUM_ROWS_EXAMINED                  AS rows_examined,
    SUM_ROWS_SENT                      AS rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

重点关注 rows_examined / rows_sent 的比值。 这个比值是 1000,意味着扫了 1000 行才返回 1 行——扫描效率极低,优化空间大。比值接近 1,说明已经在精准定位,再优化收益有限。

二、索引失效的六种写法

这六种写法在生产代码里极其常见,且都能被静态检查发现。

1. 隐式类型转换(最常见)

-- phone 是 varchar,传了数字字面量
SELECT * FROM users WHERE phone = 13800000000;   -- 索引失效
SELECT * FROM users WHERE phone = '13800000000'; -- 索引生效

原因是 MySQL 会把列转换成数字再比较,列上套了转换就等于套了函数,B+ 树的有序性用不上。Java 里 Long 字段映射到 varchar 列,是最容易出这个问题的地方。

2. 列被函数包裹

-- 失效
SELECT * FROM orders WHERE DATE(create_time) = '2026-05-01';

-- 生效:改成范围查询
SELECT * FROM orders
WHERE create_time >= '2026-05-01 00:00:00'
  AND create_time <  '2026-05-02 00:00:00';

MySQL 8.0.13 之后也可以建函数索引来解决,但优先推荐改写成范围查询——它对所有版本都有效,也更利于优化器估算。

3. 前导模糊匹配

WHERE name LIKE '%张%'    -- 失效
WHERE name LIKE '张%'     -- 生效

% 在前意味着无法定位 B+ 树的起始点。真要做全文匹配,应该上全文索引或者专门的搜索引擎,而不是指望 B+ 树。

4. 联合索引不满足最左前缀

索引 (a, b, c):

条件 能否用上索引
a = ? AND b = ? 能
b = ? AND c = ? 不能(缺最左列)
a = ? AND c = ? 只能用到 a
a = ? AND b > ? AND c = ? c 用不上(范围之后的列失效)

最后一行是最容易被忽略的:范围条件之后的列无法继续用于索引查找。如果 b 的等值条件比 c 更重要,就要调整联合索引的列顺序。

5. OR 连接了无索引的列

WHERE indexed_col = 1 OR unindexed_col = 2

一侧没有可用索引,优化器往往直接退化成全表扫描。改写成 UNION ALL 通常能救回来。

6. 排序与索引顺序不一致

-- 索引 (a, b),但按 b 排序
SELECT * FROM t WHERE a = 1 ORDER BY b;   -- 能用索引
SELECT * FROM t WHERE a = 1 ORDER BY c;   -- 触发 filesort

EXPLAIN 的 Extra 列里出现 Using filesort 或 Using temporary,就是排序/分组没走上索引。

三、为什么加了索引反而更慢

这才是本文的重点。加索引变慢,通常是下面四个原因之一。

原因 1:优化器选错了索引

统计信息过期、数据倾斜、或者索引太多导致估算成本出错,优化器就可能选一个更差的执行计划。

验证方法:不要猜,直接对比。

EXPLAIN SELECT ... ;                    -- 看优化器选了什么
EXPLAIN SELECT ... FORCE INDEX(idx_x);  -- 强制用你以为对的索引

如果 FORCE INDEX 明显更快,说明是统计信息或选择性问题:

ANALYZE TABLE orders;   -- 先更新统计信息,多数情况能解决

原因 2:回表代价超过了全表扫描

这是最反直觉的一条。二级索引查到主键后,还要回表去聚簇索引取整行数据。如果命中行数很多,回表就是大量随机 IO;而全表扫描是顺序 IO,在机械盘或者数据量不大时反而更快。

这就是"加了索引反而更慢"的典型场景:索引的选择性不够好,扫了几万行再回表几万次。

解决办法是覆盖索引——把 SELECT 需要的列都放进索引,彻底消除回表:

-- 原查询:需要回表取 status、amount
SELECT id, status, amount FROM orders WHERE user_id = ?;

-- 覆盖索引:三列都在索引里,不用回表
ALTER TABLE orders ADD INDEX idx_user_status_amount (user_id, status, amount);

EXPLAIN 的 Extra 显示 Using index,就说明命中了覆盖索引。

原因 3:选择性太低的列不该建索引

-- 区分度计算
SELECT COUNT(DISTINCT status) / COUNT(*) FROM orders;

如果结果是 0.0001(只有几个状态值),那么在这个列上建单列索引几乎没用——扫 30% 的行,回表 30% 的行,比全表扫描还亏。

区分度低于 0.1 的列,不要单独建索引,它只适合作为联合索引里的辅助列。

原因 4:索引太多,拖垮了写入

索引不是免费的:每个索引都要在 INSERT / UPDATE / DELETE 时同步维护,还要占缓冲池。

一个高频写入的表上挂了 12 个索引,写入性能下降是必然的。更糟的是,索引页会挤占缓冲池,导致真正需要缓存的数据页被换出,读性能一起变差。

治理动作:定期盘点从未被使用过的索引。

SELECT * FROM sys.schema_unused_indexes;

sys 库里的这张视图会列出索引统计中从未被使用的索引——注意它统计的是实例重启以来的使用情况,所以结论要结合业务周期看(比如季度报表用的索引,平时确实"未使用")。

四、两个立竿见影的手法

1. 覆盖索引

如上,把 SELECT 的列纳入索引。代价是索引变大,适合读多写少的表。

2. 延迟关联

深分页场景的经典问题:

-- 慢:扫描并回表 100 万行,只为返回 20 行
SELECT * FROM orders ORDER BY create_time DESC LIMIT 1000000, 20;

-- 快:先用覆盖索引拿主键,再回表取这 20 行
SELECT o.* FROM orders o
JOIN (
    SELECT id FROM orders ORDER BY create_time DESC LIMIT 1000000, 20
) t ON o.id = t.id;

子查询里 id 和 create_time 都在索引上,不回表;外层只回表 20 次。深分页从几十秒降到几百毫秒,往往只需要这一处改写。

五、验证:优化读的时候别忘了写

每次索引变更都要看四个指标,缺一个结论都不成立:

指标 关注点
目标 SQL 的执行时间 P95 优化是否生效
rows_examined / rows_sent 扫描效率是否改善
缓冲池命中率 1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests
写入 QPS 与写入延迟 加索引的代价,最容易被忽略

加索引是"读换写"的交易。 只在测试环境验证查询变快,不看写入指标,是索引治理里最常见的疏漏——线上写入高峰期出问题时,往往已经过去好几天了。

另外,DDL 本身要挑时间窗口。大表加索引会占用资源,优先用支持在线 DDL 的方式,并避开业务高峰。

六、小结

四句话:

  1. 挑 SQL 按"平均耗时 × 次数"排序,不要只看慢日志——最快的那批 SQL 往往不在慢日志里。
  2. 索引失效的六种写法都是可静态检查的,把它们做成代码扫描规则,比事后救火划算得多。
  3. 加了索引反而更慢,多半是回表代价超过了全表扫描,覆盖索引是首选解法。
  4. 索引是读换写的交易,验证时必须同时看写入指标和缓冲池命中率。

如果只记一句:优化的顺序永远是"先挑对 SQL,再看执行计划,最后才动索引"。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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