pt-duplicate-key-checker可快速识别结构冗余索引,但仅做静态比对、不分析实际查询使用;需结合information_schema.STATISTICS手动验证、performance_schema索引使用统计及EXPLAIN执行计划综合判断冗余性。

直接用 pt-duplicate-key-checker 扫一遍最省事
它能立刻识别出结构上完全等价或前缀包含的索引,比如 INDEX(a) 和 INDEX(a, b)、UNIQUE(a) 与 INDEX(a) 这类组合。但要注意:它不分析查询实际是否用到这些索引,只做静态结构比对。
运行前确认 Percona Toolkit 已安装(如 pt-duplicate-key-checker --version 能返回版本号),然后执行:
pt-duplicate-key-checker --database=your_db --user=root --password='xxx'
常见误判点:
- 它默认只比较同类型索引(比如都为
BTREE),不会把FULLTEXT和BTREE当作冗余 - 对 MySQL 8.0+ 隐式主键表(无显式
PRIMARY KEY的 InnoDB 表)判断可能不准,建议先SHOW CREATE TABLE确认主键结构 - 输出里标为「duplicate」的索引可直接删;标为「redundant」的需结合查询验证再删,不能照单全收
查 information_schema.STATISTICS 手动比对字段顺序
当无法装外部工具,或想确认某张表具体哪些列被重复索引时,必须查 information_schema.STATISTICS,重点看 SEQ_IN_INDEX 和 COLUMN_NAME 的组合是否完全一致。
执行这条语句可列出同一张表上字段顺序完全相同的索引对:
SELECT s1.TABLE_NAME, s1.INDEX_NAME AS idx1, s2.INDEX_NAME AS idx2
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
AND s1.INDEX_NAME != s2.INDEX_NAME
WHERE s1.TABLE_SCHEMA = 'your_db'
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
)
AND COUNT(*) = (
SELECT COUNT(*) FROM information_schema.STATISTICS s4
WHERE s4.TABLE_SCHEMA = s1.TABLE_SCHEMA
AND s4.TABLE_NAME = s1.TABLE_NAME
AND s4.INDEX_NAME = s2.INDEX_NAME
);
关键注意点:
-
SEQ_IN_INDEX = 1不代表就是最左前缀——必须按序比对全部字段,比如(a,b)和(a,c)就不算冗余,因为第二列不同 - InnoDB 表中,二级索引自动包含主键列,所以
INDEX(a, id)很可能是冗余的(id是主键),但得先确认主键名和类型 - 结果里出现多个
idx1/idx2组合时,说明这张表存在多组冗余,优先删掉单字段索引(如INDEX(a)),保留联合索引(如INDEX(a,b))
别光看 performance_schema.table_io_waits_summary_by_index_usage 的 COUNT_READ = 0
这个视图里 COUNT_READ = 0 并不等于“索引没用”,它漏记太多场景:ORDER BY、GROUP BY、覆盖扫描、外键约束、FORCE INDEX、低频后台任务,甚至某些带函数的 WHERE 条件(如 WHERE DATE(created_at) = ...)也绕过统计但依赖索引。
想确认一个索引是否真没人用,分三步走:
- 先查
SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'performance_schema';,结果必须是ON - 再开消费者:
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME IN ('events_waits_current', 'events_statements_history_long'); - 最后开采集器:
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME LIKE 'wait/io/table/%';
做完这三步,至少等满一个完整业务周期(建议 24 小时以上),再查该视图才可信。否则所有 COUNT_READ = 0 都只是“没统计到”,不是“没用过”。
用 EXPLAIN FORMAT=TRADITIONAL 看真实执行路径
很多索引不显现在 key 字段里,却支撑排序或覆盖扫描。这时候得盯 Extra 字段:
-
Using index表示走了覆盖索引,哪怕key是NULL,也不能删 -
Using filesort消失,往往正依赖某个索引做排序,比如SELECT id, name FROM users ORDER BY created_at LIMIT 10—— 删掉(created_at)索引就会回归全表扫描 + filesort - 无 WHERE 条件的查询最容易被忽略,但它们恰恰是排序/分组类索引的主要使用场景
真正危险的是那种既不进 key、也不触发 Using index、还不影响 Using filesort 的索引——这种才值得深入排查是否冗余。但只要有一处查询路径依赖它,就不能动。


















