索引在写多读少场景下是硬性开销放大器,每增一个索引都会线性增加i/o、cpu、锁等待及写放大;危险索引包括时间单列、长前缀、冗余、低选择性索引;应通过sys.schema_unused_indexes等工具定位并精简。

写多读少场景下,索引不是“锦上添花”,而是写入路径上的硬性开销放大器——每条INSERT/UPDATE/DELETE都必须同步刷多棵B+树,没有例外。
为什么写操作会随索引数量线性变慢
InnoDB对每个索引的维护不是“可选动作”,而是事务提交前的强制步骤:写数据页、写索引页、写redo日志(含索引变更)、刷脏页。索引越多,单次写入触发的磁盘I/O和CPU计算越重。
- 每多一个索引,
Innodb_pages_written就多一次落盘,Innodb_buffer_pool_write_requests也同步增长 -
Innodb_rows_inserted与Innodb_pages_written比值跌破5,说明写入1行却刷了20+页——大概率是索引在拖后腿 - 锁等待时间明显拉长:二级索引更新需获取
LOCK_ORDINARY,多个索引 = 多次加锁/解锁,死锁概率指数上升
哪些索引在写多场景下最危险
它们看起来“有用”,实则对写入纯属负贡献:
-
created_at或updated_at单列索引:写入必更新,但查询极少只按时间点查(通常带其他条件) - 前缀过长的索引,如
INDEX (content(255)):索引体积大、更新成本高,而实际WHERE content LIKE 'xxx%'几乎用不到255长度 - 冗余索引,如已有
(user_id, status),又单独建了(user_id):后者完全被覆盖,却额外消耗写资源 - 低选择性字段索引,如
status ENUM('pending','done','failed')单独建索引:区分度差,优化器常弃用,但写入照刷不误
如何快速定位并砍掉无效索引
别靠经验猜,用MySQL自带工具验证:
- 查未使用索引:
SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 'your_db';(MySQL 8.0+) - 看索引膨胀:
SHOW TABLE STATUS LIKE 'your_table';中若Index_length≥Data_length,说明索引已吃掉和数据一样多的磁盘 - 观察写放大:
SHOW GLOBAL STATUS LIKE 'Innodb_rows_%';和SHOW GLOBAL STATUS LIKE 'Innodb_pages_written';算比值 - 删之前先禁用:
ALTER TABLE t DROP INDEX idx_name;注意大表会锁表,务必在低峰执行
复合索引改写比删除更值得优先尝试
保留必要查询能力,但让索引变“轻”:
- 把宽索引
(a,b,c,d,e)精简为(a,b,c),前提是业务查询只用到前三列 - 用
TINYINT替代VARCHAR做状态索引字段,体积小、比较快、刷脏页压力小 - 避免函数索引陷阱:
WHERE YEAR(created_at) = 2024会让整个索引失效,改用WHERE created_at >= '2024-01-01' AND created_at
真正难的不是“删哪个”,而是接受一个事实:写多场景下,索引不是越多越好,而是越精准、越窄、越少越好。一旦开始依赖索引合并或全索引扫描来补救,说明问题已经从性能抖动滑向雪崩边缘。











