二级索引越多,写性能越差:主键写1次,n个二级索引需额外n次b+树维护,引发随机i/o、锁竞争、缓冲池挤占、优化器误判及ddl耗时剧增。

INSERT/UPDATE/DELETE要同步更新所有二级索引的B+树
每次写操作不是只改一行数据,而是要为每个二级索引单独执行一次B+树插入或更新。主键索引写1次,N个二级索引就得再写N次——8个索引意味着至少9次独立的树结构维护动作。这些操作不是顺序追加,而是随机定位、页分裂、指针重排、脏页刷盘,每一步都消耗CPU、内存锁和磁盘I/O。
常见错误现象:SHOW PROCESSLIST里大量线程卡在update或insert状态;innodb_row_lock_waits指标飙升;单条UPDATE user SET updated_at = NOW() WHERE id = 123耗时从2ms涨到50ms。
- 唯一索引(
UNIQUE INDEX)额外增加存在性校验,且无法被Change Buffer缓存,必须同步落盘 - 含JSON生成列的索引,每次JSON字段变更都会触发生成列重算 + 所有依赖索引更新
- 前缀索引(如
INDEX idx_title (title(100)))虽省空间,但更新时仍需刷新整节点,分裂更频繁
高并发下锁竞争呈指数级放大
每个索引更新都需要对对应B+树路径加闩锁(latch),不是行锁也不是表锁,是更底层的结构锁。索引越多,多线程同时写同一行时,争抢不同索引树路径的概率越高,容易形成锁等待链甚至死锁。
实测案例:订单表有12个二级索引,QPS 200+时Lock wait timeout exceeded错误频发;删掉4个低频索引后,同样压力下事务失败率归零。
- 不是所有索引锁冲突概率相同:高频更新字段(如
status、updated_at)上的索引,锁热点最集中 -
ALGORITHM=INPLACEDDL虽不锁表,但仍需获取元数据锁(MDL),索引越多,等待窗口越长 - Buffer Pool中索引页占比过高(比如超40%),会挤占数据页缓存,间接加剧物理I/O争抢
优化器成本模型失真导致“越建越慢”
MySQL优化器评估执行计划时,要遍历所有可用索引计算代价。索引超10个后,路径组合爆炸,统计信息(Cardinality)更新滞后,容易选错索引——比如本该走(a, b)却用了(a),结果触发Using filesort或临时表,查询变慢后开发者又加索引补救,恶性循环。
典型表现:EXPLAIN显示key_len合理但rows预估偏差巨大;ANALYZE TABLE耗时明显增长;information_schema.STATISTICS里多个索引Cardinality接近0。
- 联合索引顺序不合理(如把
is_deleted放最左)会让树深度陡增,写入路径拉长 - 冗余索引(已有
(a, b, c)还建(a, b))看似无害,实则每个写操作都白跑一次B+树维护 - MySQL 8.0+的
sys.schema_unused_indexes可直接查出长期rows_selected = 0的索引,但要注意ORM可能隐式依赖某些“未显式使用”的索引
DDL操作时间与索引数量线性相关
ALTER TABLE加字段、改类型、甚至只是ADD INDEX,InnoDB默认需重建整张表及其全部索引。10个索引 × 全表扫描 × B+树重建 = 实际I/O压力翻10倍。
生产事故记录:12个索引的订单表执行MODIFY COLUMN amount DECIMAL(12,2),耗时47分钟,期间写入堆积,应用因超时熔断。
-
ALGORITHM=INPLACE能避免全表拷贝,但仍要为每个索引单独做online rebuild,CPU和内存开销线性增长 -
OPTIMIZE TABLE本质也是重建所有索引,大表慎用;它不会减少索引数量,只会让现有索引更紧凑 - 备份恢复时间也随索引体积扩大——一个10GB表配20GB索引,xtrabackup全量备份时间几乎翻倍
UNIQUE索引或含JSON生成列的索引——它们无法被缓冲,强制同步执行,成了整个写入链路上的硬瓶颈。删索引前务必确认performance_schema.table_io_waits_summary_by_index_usage里它的使用率为0,否则可能把一个慢查询变成更慢。











