很多 MySQL 表跑久了以后,索引会越长越多。业务改版留下一批历史索引,临时需求加了一批组合索引,最后每次写入都在替过去的查询买单。但直接删索引又很吓人:万一某个低频报表或后台任务还依赖它,线上慢查询会马上找上门。
我更喜欢先用 MySQL 的不可见索引做一次预演。把索引设为 INVISIBLE 后,优化器默认不会使用它,但 InnoDB 仍然维护这个索引。也就是说,它很适合验证“查询还需不需要这个索引”,不适合验证“删掉以后写入能省多少成本”。这个边界一定要先讲清楚。
不可见索引到底解决什么问题
它解决的是删索引前的风险评估。传统做法是先在测试环境看执行计划,再在线上低峰直接 DROP INDEX。问题是测试环境的数据分布、SQL 覆盖率、业务流量都很难和线上一致。不可见索引提供了一个中间态:索引还在,回滚很快,但优化器先把它当作不存在。
这对于治理冗余索引尤其有价值。比如 (user_id, created_at) 和 (user_id) 同时存在,你怀疑后者已经没意义;或者某个老活动索引长期没有命中,但谁也不敢删。先隐藏,再观察真实流量,判断会稳得多。
先找候选索引,不要全表乱试
我一般从三类索引入手:重复前缀索引、长期没有被执行计划命中的索引、写入成本明显但查询收益存疑的索引。候选出来以后,先记录当前表结构和关键 SQL 的执行计划。
SHOW INDEX FROM orders; EXPLAIN FORMAT=TREE SELECT * FROM orders WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 20;
如果你们有 SQL 指纹和慢查询平台,最好把候选索引可能影响的 SQL 都列出来。不要只测一个接口就认为万事大吉,后台任务、报表、客服查询往往才是低频但重的那一类。
隐藏索引的 SQL 很简单,难的是观察
把索引设为不可见通常只需要一条 DDL:
ALTER TABLE orders ALTER INDEX idx_user_created INVISIBLE;
隐藏后我会至少观察一个完整业务周期。电商系统要覆盖晚高峰和定时任务,SaaS 系统要覆盖工作日白天,报表系统要覆盖日结或周结。观察项包括慢查询数量、rows examined、执行计划变化、接口 p95/p99、数据库 CPU 和临时表情况。
发现问题时,回滚也很直接:
ALTER TABLE orders ALTER INDEX idx_user_created VISIBLE;
别把不可见索引用错成性能优化
一个常见误会是:把索引设为不可见以后,写入就会变快。不是这样。只要索引还存在,DML 仍然需要维护它。不可见索引省掉的是优化器选择它的路径,不是省掉索引维护成本。真正要拿到写入收益,最终还是要 DROP INDEX。
所以这套流程应该分两段:第一段隐藏索引,验证查询不依赖;第二段删除索引,验证写入收益和空间释放。两段中间最好隔开,留出足够观察时间。
上线检查清单
- 确认目标库是 MySQL 8.x,并确认主从、备份、灰度环境版本一致。
- 记录候选索引、可能受影响 SQL、当前执行计划和慢查询基线。
- 低峰执行
ALTER INDEX ... INVISIBLE,并准备VISIBLE回滚语句。 - 观察至少一个完整业务周期,不只看 10 分钟内的接口流量。
- 确认没有计划退化、慢查询增长、rows examined 暴涨,再安排真正删除。
- 删除前再次确认备份和变更窗口,因为 DROP 后回滚成本完全不同。
我的经验结论
不可见索引最适合的场景不是炫技,而是把“删索引”从一次不可逆动作,拆成一次可回滚实验。它让你先回答一个问题:如果优化器不能用这个索引,线上查询会不会出事?
答案稳定以后,再讨论删除索引带来的写入收益和空间收益。这个顺序看起来慢一点,但数据库变更最怕的就是自信过头。能灰度的地方先灰度,能回滚的地方先保留回滚,这才是生产 MySQL 索引治理的基本体面。

JFR 排查 Spring Boot 慢接口:别急着加缓存,先抓一段 Flight Recording
