直接删INDEX(a)而保留INDEX(a,b)通常是安全的,因后者能完全覆盖前者功能;但需用sys.schema_redundant_indexes视图或手动比对information_schema.statistics中相同起始列组合来确认冗余,严禁仅凭未使用记录删除。

直接删 INDEX(a) 而保留 INDEX(a, b) 通常是安全的,但仅凭“没被查过”就删索引,90% 的线上慢查询爆发都源于此误操作。
怎么快速揪出真正冗余的索引
冗余不是“长得像”,而是“功能被完全覆盖”。INDEX(a, b) 能干 INDEX(a) 干的所有活(WHERE a = ?、ORDER BY a),后者就是冗余;但 INDEX(a, b) 和 INDEX(a, c) 不冗余,因为 b 和 c 无法互换。
- 用
sys.schema_redundant_indexes视图(MySQL 8.0+)直接列出前缀覆盖关系:它会标出哪些索引可被另一个更宽的索引替代 - 手动比对时,重点看
information_schema.statistics中同一张表下、相同起始列组合的索引,例如idx_user_id和idx_user_id_status_created—— 前者极大概率冗余 - 别信
SHOW INDEX输出顺序,它不体现逻辑覆盖;必须按seq_in_index排序后逐字段比对列名
为什么 schema_unused_indexes 不能直接当删除清单
sys.schema_unused_indexes 只返回“自 performance_schema 启用以来从未被优化器选中”的索引,但它不区分场景:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 该索引可能只在凌晨报表中使用,白天流量里根本不会触发
- 某些
ORDER BY或GROUP BY查询即使没WHERE条件,也依赖索引避免Using filesort,但COUNT_READ = 0 - 外键隐式创建的索引(如
FOREIGN KEY (user_id) REFERENCES users(id))也会出现在这个视图里,但删掉会导致INSERT/UPDATE报错 - 重启 MySQL 后,该视图数据清零,短期采集结果不可靠
删之前必须验证的三件事
跳过任何一项,都可能让某个核心接口响应时间从 50ms 暴涨到 2s。
- 查外键依赖:
SELECT CONSTRAINT_NAME, COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'db' AND TABLE_NAME = 't' AND COLUMN_NAME = 'xxx';—— 如果有结果,说明该列上有外键,对应索引不能动 - 搜应用层硬编码:
grep -r "FORCE INDEX\|USE INDEX" ./src或检查 SQL 代理层日志,确认没有 ORM 或中间件显式指定该索引名 - 重跑核心 SQL 的
EXPLAIN FORMAT=TRADITIONAL,把疑似索引临时禁用:ALTER TABLE t DROP INDEX idx_name;,再执行一遍,观察key字段是否变化、rows是否飙升、是否出现Using temporary或Using filesort
大表删索引时最容易卡住的点
看起来只是删元数据,但在千万级表上,DROP INDEX 仍可能锁表或拖垮主从。
- MySQL 5.6+ 支持
ALGORITHM=INPLACE,但需确保innodb_online_alter_log_max_size足够大,否则退化为 copy 表,磁盘空间翻倍、耗时数小时 - 主从架构下,DDL 写入 binlog 后,从库回放期间若索引还存在,不会报错;但如果从库 MySQL 版本低(如 5.7 主库 + 5.6 从库),可能因语法不兼容卡死
- 真正危险的是“删完没立刻出问题,但某次统计信息更新后优化器突然换执行计划”——所以删完至少观察 48 小时,尤其盯
Slow_queries和Handler_read_*状态变量突增
最常被忽略的是复合索引里字段顺序的语义:删掉 INDEX(a, b) 改建 INDEX(b, a) 看似等价,但所有 WHERE a = ? AND b > ? 查询都会退化成全索引扫描。冗余判断必须绑定具体查询模式,脱离业务 SQL 的索引分析都是纸上谈兵。

















