单表索引超5–6个后写入性能显著下降,因每次INSERT/UPDATE/DELETE需同步更新所有二级索引的B+树,引发多次磁盘I/O、页分裂及锁竞争,高并发下易现超时与死锁。

单表索引数量不是“不能超”,而是超过 5–6 个后,INSERT、UPDATE、DELETE 的性能衰减会变得肉眼可见——尤其在高并发写入或大字段更新场景下。
为什么每次写操作都要更新所有相关索引
MySQL 的二级索引(非主键索引)存储的是「索引列值 + 主键值」,不是整行数据。所以每次修改某行时:
- InnoDB 必须定位到该行的聚簇索引(主键)位置,读出主键值;
- 再根据这个主键值,分别去更新每一个二级索引中对应的叶子节点(B+树);
- 每个索引更新都涉及 B+树分裂、页合并、缓冲池刷脏等动作,不是简单赋值。
比如一个 UPDATE user SET status = 1 WHERE id = 123,若该表有 8 个二级索引,InnoDB 实际要执行 8 次独立的索引树维护操作,而不仅仅是改一行数据。实测中,6 个索引可能让写延迟翻倍,10 个以上常出现 Lock wait timeout exceeded 报错。
索引过多如何拖慢 DDL 操作
ALTER TABLE 加字段、改类型、甚至只是 ADD INDEX,只要涉及表结构变更,InnoDB 默认会重建整张表(含所有索引)。索引越多,重建耗时越长,且期间表被锁(ALGORITHM=COPY 模式下是全表锁)。
-
ADD INDEX本身就要扫描全表构建新索引树,索引数 × 数据量 = 实际 I/O 压力; - 在线 DDL(
ALGORITHM=INPLACE)虽能减少锁,但依然需要为每个索引单独做 online rebuild,内存和 CPU 开销线性增长; - 生产环境曾有案例:12 个索引的订单表执行
MODIFY COLUMN,耗时 47 分钟,期间写入堆积导致应用超时熔断。
哪些索引最容易成为“隐形写入瓶颈”
不是所有索引对写入的影响都一样。以下几类索引在 DML 中开销特别高:
-
TEXT或VARCHAR(2000)字段上的前缀索引:虽然节省空间,但 B+树节点里存的是前缀哈希或截断值,更新时仍需重新计算并刷新整节点; - 包含
JSON字段的生成列索引:每次 JSON 内容变动,生成列值重算 + 所有依赖它的索引同步更新; - 高频更新字段上的索引(如
updated_at、status):哪怕只改一个时间戳,也会触发所有含该字段的联合索引更新; - 冗余索引,例如已有
(a, b),又单独建了(a):写入时两个索引都要维护,但查询时优化器通常只用其中一个。
用 SELECT * FROM information_schema.STATISTICS WHERE table_name = 'your_table' AND index_schema = 'your_db' 定期查索引定义,比对着 SHOW INDEX 更容易发现重复或低效组合。
怎么判断当前索引是否真正在“拖后腿”
别只看 SHOW INDEX,得结合实际负载看效果:
- 查
information_schema.INNODB_METRICS中的index_lock_wait_count和index_lock_wait_time,飙升说明索引更新锁竞争严重; - 用
pt-index-usage或performance_schema.table_io_waits_summary_by_index_usage看各索引的实际命中率,长期count_star = 0的索引就是纯写入负担; - 对比慢日志中
UPDATE/DELETE的平均执行时间与索引数量趋势,如果加第 7 个索引后写入 P95 延迟跳升 30% 以上,基本可以确认拐点已到。
真正难处理的不是“要不要删索引”,而是业务方总说“这个查询下周就要上线”。所以建议把索引评审纳入上线 checklist,而不是等 ALTER TABLE 卡住才想起看执行计划。











