华为云RDS慢查询治理与索引失效定位
华为云 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 的方式,并避开业务高峰。
六、小结
四句话:
- 挑 SQL 按"平均耗时 × 次数"排序,不要只看慢日志——最快的那批 SQL 往往不在慢日志里。
- 索引失效的六种写法都是可静态检查的,把它们做成代码扫描规则,比事后救火划算得多。
- 加了索引反而更慢,多半是回表代价超过了全表扫描,覆盖索引是首选解法。
- 索引是读换写的交易,验证时必须同时看写入指标和缓冲池命中率。
如果只记一句:优化的顺序永远是"先挑对 SQL,再看执行计划,最后才动索引"。
- 点赞
- 收藏
- 关注作者
评论(0)