能安全删除的冗余索引必须同时满足未被任何查询使用且功能被另一索引完全覆盖;仅列组合完全重复的索引可直接删,前缀覆盖型(如INDEX(a) vs INDEX(a,b))需结合EXPLAIN验证,运行时统计仅作参考,删前须检查硬编码、主从兼容性并逐个验证。

能安全删的冗余索引,必须同时满足两个条件:没被任何查询实际用到,且功能完全被另一个索引覆盖。只看“长得像”就删,大概率引发慢查询爆发。
查列组合完全重复的索引
这是最基础也最容易漏掉的一类——多个索引定义的列顺序和内容一模一样。MySQL 允许建,但毫无意义。
- 执行
SELECT table_name, index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols FROM information_schema.statistics WHERE table_schema = 'your_db' GROUP BY table_name, cols HAVING COUNT(*) > 1;,结果里的每个index_name都是明确可删的候选 - 注意
GROUP_CONCAT默认长度是 1024,如果索引列太多可能被截断,建议先设大一点:SET SESSION group_concat_max_len = 10000; - 这个 SQL 不会识别
INDEX(a)和INDEX(a,b)的覆盖关系,它只抓“完全一致”的副本
识别前缀覆盖型冗余(如 INDEX(a) vs INDEX(a,b))
复合索引的前缀天然具备独立索引的功能,但反过来不成立。判断关键在字段顺序和查询模式。
-
INDEX(a,b)能支撑WHERE a = ?、WHERE a = ? AND b = ?、ORDER BY a,b;而INDEX(a)只能撑前两者中的第一个 - 如果线上所有查
a的语句都带b条件,或都走INDEX(a,b)(用EXPLAIN确认),那INDEX(a)就是冗余的 - 反例:
INDEX(a,b)和INDEX(a,c)不冗余——WHERE a=1 AND c=2走不了前者,别手滑删错
确认索引是否真没被用过
performance_schema.table_io_waits_summary_by_index_usage 是 MySQL 8.0+ 最可靠的运行时依据,但它有陷阱。
- 执行
SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME FROM performance_schema.table_io_waits_summary_by_index_usage WHERE INDEX_NAME IS NOT NULL AND COUNT_READ = 0 AND COUNT_INSERT = 0 AND COUNT_UPDATE = 0 AND COUNT_DELETE = 0;,结果只是“近期未命中”,不是“永远不用” - 该表在 MySQL 重启后清零,且默认不开启监控项;需确保
setup_consumers中对应项为ENABLED - 更稳妥的做法:对核心业务 SQL 手动
EXPLAIN,再临时DROP INDEX idx_name ON tbl,观察执行计划中key字段是否变化;若不变,说明那个索引确实没参与决策
删之前必须绕开的三个坑
删除操作本身简单,但线上环境里最容易栽在细节上。
- ORM 或存储过程中硬写了
FORCE INDEX(idx_name)?删了直接报错,得全量 grep 代码库 - 主从架构下,
DROP INDEX是 DDL,会写入 binlog;如果从库 MySQL 版本低于 5.6,或者innodb_online_alter_log_max_size不够,可能卡住复制 - 哪怕确认冗余,也别一次删多个——尤其在大表上。每次只删一个,观察 1–2 个业务高峰周期的慢查询日志和 QPS 波动
真正难的从来不是“怎么找”,而是“怎么证伪”:证明某个索引删了不会影响任何路径。很多所谓“冗余”只在定时任务、管理后台或异常分支里被调用,平时根本看不到流量。别跳过验证环节。



















