直接删INDEX(a)而保留INDEX(a,b)通常安全,但需经使用统计、执行计划、外键依赖三重验证;MySQL 8.0+可用sys.schema_unused_indexes查未用索引,但须启用performance_schema对应消费者,并额外检查外键隐式索引及代码中FORCE INDEX调用。

直接删 INDEX(a) 而保留 INDEX(a, b) 通常是安全的,但「无用」不等于「可删」——很多索引在白天流量里不显山露水,却支撑着凌晨报表、对账或唯一约束校验。必须结合使用统计、执行计划和外键依赖三重验证,否则一删就出慢查询。
查哪些索引真没被用过
MySQL 8.0+ 可直接用 sys.schema_unused_indexes,但它只统计重启后、且 performance_schema 开启对应消费者的状态。别跳过前置检查:
- 确认已启用关键消费者:
SELECT * FROM performance_schema.setup_consumers WHERE NAME IN ('events_statements_history_long', 'table_io_waits_summary_by_index_usage');,若为DISABLED,需先UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = '...'; -
sys.schema_unused_indexes不包含被外键隐式创建的索引,得额外查:SELECT CONSTRAINT_NAME, COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table' AND CONSTRAINT_NAME LIKE 'fk_%'; - 该视图返回的索引,仍可能被
FORCE INDEX或 ORM 自动生成的 hint 强制调用,grep 代码库里的 SQL 字符串比看统计更可靠
识别冗余而非重复的索引
冗余不是字段一样,而是功能被覆盖。比如 INDEX(user_id) 和 INDEX(user_id, status) 共存时,前者就是冗余的;但 INDEX(user_id, status) 和 INDEX(user_id, created_at) 并不冗余——因为 status 和 created_at 无法互相替代。
- 用这条 SQL 快速揪出前缀覆盖关系:
SELECT s1.TABLE_NAME, s1.INDEX_NAME AS redundant_idx, s2.INDEX_NAME AS covering_idx FROM information_schema.STATISTICS s1 JOIN information_schema.STATISTICS s2 ON s1.TABLE_SCHEMA = s2.TABLE_SCHEMA AND s1.TABLE_NAME = s2.TABLE_NAME AND s1.SEQ_IN_INDEX = s2.SEQ_IN_INDEX AND s1.COLUMN_NAME = s2.COLUMN_NAME WHERE s1.TABLE_SCHEMA = 'your_db' AND s1.INDEX_NAME != s2.INDEX_NAME GROUP BY s1.TABLE_NAME, s1.INDEX_NAME, s2.INDEX_NAME HAVING COUNT(*) = (SELECT COUNT(*) FROM information_schema.STATISTICS s3 WHERE s3.TABLE_SCHEMA = s1.TABLE_SCHEMA AND s3.TABLE_NAME = s1.TABLE_NAME AND s3.INDEX_NAME = s1.INDEX_NAME); - 注意隐式前缀:已有
INDEX(a(10), b),再建INDEX(a(5))仍是冗余;但INDEX(a(5))和INDEX(a(10))不构成覆盖,不能简单划等号 -
pt-duplicate-key-checker能自动标出这类关系,比手写 SQL 更稳,尤其适合跨表批量扫描
删之前必须跑通的三步验证
跳过任何一步,都可能让某条 JOIN 查询从 20ms 暴涨到 2s。这不是理论风险,是线上高频事故。
MySQL 数据库管理技能,能够通过自然语言查询和管理数据库,支持 SELECT/INSERT/UPDATE/DELETE、表管理、备份恢复等操作。当用户提及 MySQL、数据库查询、建表或数据备份时使用此技能。
- 对核心 SQL 手动禁用待删索引:
ALTER TABLE t DROP INDEX idx_a; EXPLAIN SELECT * FROM t WHERE a = 1 AND b = 2;,确认key字段没退化成NULL或换到低效索引 - 检查 Handler 指标是否突增:
SHOW GLOBAL STATUS LIKE 'Handler_read%';,删完后观察Handler_read_next和Handler_read_rnd_next是否明显升高——这是回表或文件排序变多的信号 - 确认没被唯一性保障依赖:执行
SHOW CREATE TABLE t;,看待删索引是否出现在UNIQUE KEY或PRIMARY KEY定义中;即使名字不叫uk_,也可能是隐式创建的外键索引
主从架构下删索引的实际影响
看起来只是元数据操作,但在 MySQL 5.7 及更早版本,DROP INDEX 在大表上仍可能锁表并阻塞从库复制。尤其当主库用 ALGORITHM=COPY(因 innodb_online_alter_log_max_size 不足触发降级)时,从库回放 DDL 期间若索引还存在,不会报错,但会卡住后续事件。
- 建议节奏:
STOP SLAVE;→ 主库删索引 →START SLAVE;→ 观察Seconds_Behind_Master是否快速追平 - 避免在高峰期执行;单次只删 1 个索引,间隔至少 30 分钟,给监控系统留出反应窗口
- 删完立刻执行
ANALYZE TABLE t;,否则优化器统计信息滞后,可能导致后续查询选错执行路径
最常被忽略的是索引的“低频高危”属性:一个每月只跑一次的对账任务,可能全靠某个看似冷门的 INDEX(created_at, status) 支撑。删之前,翻慢日志里带 ORDER BY 或 LIMIT 的长耗时语句,比盯着 COUNT_STAR = 0 更管用。










