mysql 8.0+ 应优先用 sys.schema_unused_indexes 和 sys.schema_redundant_indexes 查无用索引,但必须确认 performance_schema 已采集足够时长的真实查询数据,否则结果全是假阴性;前者依赖运行时统计,后者仅基于 ddl 分析,均需人工交叉验证 cardinality、orm 隐式调用及定时任务依赖。

MySQL 8.0+ 直接用 sys.schema_unused_indexes 和 sys.schema_redundant_indexes 查无用索引,但必须确认 performance_schema 已采集足够时长的真实查询数据,否则结果全是假阴性。
查 sys.schema_unused_indexes 前先确认数据可信
这个视图依赖 performance_schema 中的运行时统计,不是“建了没用就报”,而是“在已采集的查询里一次都没被选中”。常见失效场景:
-
performance_schema默认开启,但相关 consumers 可能被关掉:需确保events_statements_history_long和events_waits_history_long已启用 - 刚重启 MySQL?所有
COUNT_FETCH归零,sys.schema_unused_indexes会把所有索引都标为“未使用” - 业务低峰期采集了 2 小时数据,但核心报表 SQL 每天只跑一次 —— 它大概率不会出现在结果里,但绝不能删
验证方式:SELECT COUNT_FETCH FROM performance_schema.table_io_waits_summary_by_index_usage WHERE OBJECT_SCHEMA = 'your_db' AND INDEX_NAME = 'idx_xxx',非零才说明真被用过。
sys.schema_redundant_indexes 不看运行时,但会漏判“部分覆盖”
它纯靠 DDL 结构分析,比如同时存在 INDEX(a) 和 INDEX(a,b),就会标记前者冗余。但它不关心后者是否真被查询用到 —— 有可能 INDEX(a,b) 也从没被用过,或者只在 WHERE a=1 ORDER BY b 这种极窄场景触发。
- 它不会报出
INDEX(a,b)和INDEX(a,b,c,d)并存时的冗余,哪怕c,d列从未出现在任何WHERE或ORDER BY中 - 联合索引列顺序错位(如
INDEX(b,a)vsINDEX(a,b))它也不识别,因为结构上不构成前缀包含关系 - 唯一约束索引(
UNIQUE KEY)和主键索引默认不进该视图,哪怕逻辑上冗余也不会提示
删索引前必须人工交叉验证的三处硬点
自动脚本输出的“可删列表”只是起点,不是判决书。重点盯:
- 该索引是否被
SELECT COUNT(*) FROM t WHERE ...类聚合查询依赖?这类语句常走索引快速计数,但不会出现在table_io_waits_summary_by_index_usage的高频读里 -
INFORMATION_SCHEMA.STATISTICS中的CARDINALITY是否极低?比如status列只有 3 个值却建了索引,CARDINALITY≈ 3,这种索引即使COUNT_FETCH=0也本就不该存在 - ORM 框架是否隐式依赖?例如 Django 的
.exists()、Laravel 的doesntExist()可能生成SELECT 1 FROM t WHERE ... LIMIT 1,而这条 SQL 的EXPLAIN可能显示走了某个你认为“没用”的索引
真正危险的不是删错一个索引,而是删掉后某条凌晨跑的报表 SQL 突然从 2 秒变成 47 秒,而你根本没在监控里设这条 SQL 的 rows 阈值告警。











