冗余索引在高并发写入时会锁住写入链路,因每条insert/update/delete需同步更新所有二级索引,8个索引中若3个冗余,则i/o、缓冲池竞争与行锁等待显著加剧,导致handler_write延迟上升和innodb_row_lock_time_avg突增。

高并发写入时,冗余索引的代价不是“慢一点”,而是“锁住写入链路”
MySQL 每次 INSERT、UPDATE、DELETE 都要同步更新所有相关索引。当一张表有 8 个二级索引,而其中 3 个是冗余的,那每条写入实际要维护 8 份 B+ 树,而不是必需的 5 份——这直接抬高了 I/O 压力、缓冲池竞争和行锁等待时间。尤其在高并发 INSERT 场景下,你看到的 Handler_write 延迟上升、Innodb_row_lock_time_avg 突增,往往就卡在这几个“没人用但必须更新”的索引上。
用 performance_schema 定位真正零使用的索引(MySQL 8.0+)
别只看 SHOW INDEX 或字段名相似就删。关键看它有没有被优化器选中过:
-
SELECT object_schema, object_name, index_name FROM sys.schema_unused_indexes WHERE object_schema = 'your_db';—— 这是最快入口,但注意:它依赖performance_schema.table_io_waits_summary_by_index_usage,该表重启即清零,且只统计开启后发生的访问 - 手动查更稳:
SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME, COUNT_STAR FROM performance_schema.table_io_waits_summary_by_index_usage WHERE INDEX_NAME IS NOT NULL AND COUNT_STAR = 0 ORDER BY OBJECT_SCHEMA, OBJECT_NAME; - 重点过滤掉主键索引(
PRIMARY)和外键隐式索引(查information_schema.KEY_COLUMN_USAGE确认是否被FOREIGN KEY引用)
识别前缀覆盖型冗余:INDEX(a) 和 INDEX(a,b) 不是“重复”,而是“可删”
这是最容易误判的点。两个索引列不完全相同,但功能重叠——INDEX(a) 在所有 WHERE a = ? 场景下,都会被 INDEX(a,b) 完全覆盖,前者就是冗余的。
- 查法:
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;找出列组合完全一致的索引(真重复) - 但前缀覆盖需人工比对:先按表分组,列出所有索引字段顺序;若存在
idx_a(a)和idx_a_b(a,b),且线上无任何查询只依赖b而不要a,那idx_a就该删 - 验证方式:临时禁用
idx_a,用EXPLAIN SELECT * FROM t WHERE a = 1;看key是否自动切到idx_a_b
删之前必须盯住三类隐式依赖,否则凌晨收告警
删索引不是元数据操作那么简单,它可能让某条报表 SQL 或 ORM 的 FORCE INDEX 直接失效。
- 查慢日志:确认近 7 天内是否有语句明确命中待删索引的
key字段(EXPLAIN输出中的key列值) - 查应用层:grep 代码库里是否有
USE INDEX、FORCE INDEX或 ORM 显式指定该索引名(如 Django 的.extra(index='idx_xxx')) - 主从兼容性:MySQL 5.7 及更早版本删索引可能触发 copy 表,建议加
ALGORITHM=INPLACE并确认innodb_online_alter_log_max_size足够;RDS 用户注意从库 binlog_format 是否为 ROW,避免回放失败
真正危险的不是“这个索引没被用”,而是“它被某个低频但关键路径硬编码依赖着”。上线前用真实业务流量压测 10 分钟,比任何静态分析都管用。











