隐藏索引是线上灰度测试索引效果最安全快捷的手段,支持不锁表、不重建、不断业务地验证删除影响;新建时加invisible,用show index确认visible: no,通过sql提示临时启用并explain对比,结合慢日志与performance_schema观测真实影响。

隐藏索引不是“关掉索引”,而是让优化器在默认情况下跳过它——但写入开销一毛不少,执行计划验证必须手动触发。
怎么创建和确认隐藏索引已生效
新建索引时直接加 INVISIBLE 最省事,ALTER 修改也只改元数据,毫秒级完成:
CREATE INDEX idx_order_status ON orders(status) INVISIBLE;
已有索引改隐藏:
ALTER TABLE orders ALTER INDEX idx_order_status INVISIBLE;
确认是否成功,别只信 SHOW INDEX 的输出,要查两处:
-
SHOW INDEX FROM orders\G中Visible: NO -
SELECT INDEX_NAME, IS_VISIBLE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = 'orders' AND INDEX_NAME = 'idx_order_status';返回NO
注意:PRIMARY KEY、第一个 UNIQUE NOT NULL 索引(隐式主键)无法设为隐藏,会报错 ER_PRIMARY_CANT_BE_INVISIBLE。
为什么 EXPLAIN 不走隐藏索引,以及怎么让它“临时现身”
默认情况下,EXPLAIN 完全无视隐藏索引,key 字段不会出现它,哪怕你写了 FORCE INDEX(idx_order_status) —— 这是设计行为,不是 bug。
想对比“有/无该索引”对执行计划的影响,必须显式启用隐形索引参与优化:
- 会话级(推荐):
SET SESSION optimizer_switch = 'use_invisible_indexes=on';,然后跑EXPLAIN SELECT ... WHERE status = 'shipped'; - SQL 级(更安全):
SELECT /*+ SET_VAR(optimizer_switch = 'use_invisible_indexes=on') */ * FROM orders WHERE status = 'shipped';再EXPLAIN
关键点:两次 EXPLAIN 必须在相同 session 下做,且确保没被其他 optimizer_switch 设置干扰(比如 use_index_extensions=off 可能掩盖真实效果)。
线上灰度时最容易漏掉的三个信号
不能只看单条 SQL 的 EXPLAIN,隐藏索引是否真可删,得靠线上真实负载反馈:
- 慢查询日志里,原本
Query_time 的语句突然变成 <code>> 500ms,且WHERE条件命中了那个隐藏索引字段 -
performance_schema.events_statements_summary_by_digest中对应 digest 的avg_timer_wait上浮 2 倍以上 - 应用代码或 ORM(如 MyBatis 的
<select></select>hint、Django 的.extra())里硬写了USE INDEX或FORCE INDEX—— 这些会绕过隐藏机制,导致索引实际仍在用
特别注意:INSERT/UPDATE/DELETE 对隐藏索引的维护照常进行,innodb_rows_updated 和磁盘 IO 不会下降。如果写多读少,隐藏后性能没变化,大概率说明它本就没人用。
为什么不能跳过隐藏直接 DROP
对千万级以上表,DROP INDEX 是 DDL 操作,需要重建表(除非用 ALGORITHM=INSTANT,但仅限加列等极少数场景),可能锁表数小时。而 ALTER INDEX ... INVISIBLE 只改 INFORMATION_SCHEMA 元数据,不碰数据页,不影响任何连接。
真正难的从来不是执行那条 INVISIBLE 语句,而是确认:有没有应用层 SQL 绕过了隐藏逻辑?有没有慢查询在灰度期被漏监控?有没有唯一约束正依赖这个索引保一致性?这些都得人工交叉验证,没法全自动。











