InnoDB更新索引字段必重写整条索引记录,引发删插两次B+树操作、锁等待上升、QPS下降30%~60%;高频更新索引字段会加剧写放大、页分裂与缓存效率下降。

UPDATE SET字段在索引里,InnoDB必须重写整条索引记录
只要该字段出现在 UPDATE 的 SET 子句中,且它属于任一二级索引(单列或复合索引的任意位置),InnoDB 就无法“局部更新”——它必须定位原索引项、删除旧节点、插入新节点。这不是简单的值覆盖,而是两次 B+ 树操作:一次删、一次插。实测中,单表每秒 UPDATE 超过 50 次时,innodb_row_lock_waits 明显上涨,SHOW PROCESSLIST 里大量线程卡在 Updating 状态。
- 哪怕只改一个字段,只要它在索引里,开销就逃不掉;别被“WHERE 没用它”误导
- 复合索引中字段位置无关紧要:哪怕
(created_at, status),只要status被改,整条索引项照样重写 - 页分裂概率陡增:高频变动导致索引页内空闲空间(gap)快速积累,
Data_free日均涨几十 MB 是典型信号
status、updated_at 这类字段加索引,QPS 实测下降 30%~60%
这不是理论推演,是真实压测结果。原因在于每次更新都强制触发:B+ 树节点定位 + 页内重排 + redo log 写入 + undo log 生成。尤其当表已有多个索引时,写放大效应成倍叠加。例如一张有 12 个索引的订单表,对 status 字段做高频更新,QPS 可能直接腰斩。
- 监控关键指标:
sys.schema_table_statistics.updates延迟、Handler_write与Handler_read_next比值突降 - 别信“读写比高就可以加”——先验证不加索引是否真走
type=all;加了之后是否稳定走ref或range - 冗余索引会白送开销:已有
INDEX(user_id, status),再建INDEX(status),写成本 +100%,查询收益 ≈ 0
怎么确认某个索引是不是“假优化、真拖累”
别靠经验猜,用数据验证。重点查三件事:它是否被 WHERE/JOIN 真正用到?更新频率是否 ≥ 1 次/秒?是否非主键、非唯一约束?
- 执行
SHOW INDEX FROM t,筛出Non_unique = 1的索引 - 查慢查询日志或
EXPLAIN结果,确认该字段在WHERE中几乎没出现(无type=ref或range) - 用
SELECT * FROM information_schema.STATISTICS WHERE table_name = 't' AND column_name = 'status'找全含该字段的索引,一个都不能漏 - 临时下线不是设
INVISIBLE——那只是骗优化器,InnoDB 依然维护;正确做法是备份后ALTER TABLE t DROP INDEX idx_status,压测 5 分钟看延迟是否下降 ≥30%
替代方案比硬扛索引开销更有效
与其在热字段上硬加索引,不如把查询压力从原表移走。最常用的是轻量级解耦,不是大动架构。
- 用生成列降低索引粒度:比如
updated_day DATE AS (DATE(updated_at)) STORED,再建INDEX idx_updated_day ON t (updated_day) - 长文本字段别用前缀索引:如
INDEX(content(255))对写入压力几乎没改善,反而削弱查询能力;改用哈希摘要:content_hash CHAR(32) AS (MD5(content)) STORED,再建INDEX(content_hash) - 高频状态类查询,优先考虑应用层双写冗余表,或基于 binlog 构建轻量物化视图,避开原表锁竞争
- MySQL 8.0.23+ 可用
ALTER TABLE t REBUILD替代OPTIMIZE TABLE:只重排页结构,不重置统计信息,避免执行计划抖动
真正容易被忽略的是:碎片、锁等待、缓冲池效率这三者从来不是孤立问题。当 innodb_buffer_pool_read_requests / innodb_buffer_pool_reads 周环比跌超 15%,说明索引已开始实质性拖垮缓存效率——这时候删索引,比调优更重要。


















