频繁更新字段加索引严重损伤写入性能:只要该字段出现在update的set子句且属于任一二级索引,innodb就必须同步重写整条索引记录,实测qps下降30%~60%,锁等待激增。

频繁更新字段加索引到底有多伤写入
直接说结论:只要字段出现在 UPDATE 的 SET 子句里,且它在任一二级索引中(单列或复合索引的任意位置),InnoDB 就必须同步重写整条索引记录。这不是“稍微慢点”,而是每次更新都触发 B+ 树节点定位、页内重排、redo log 写入、undo log 生成——实测 QPS 下降 30%~60%,innodb_row_lock_waits 暴涨,SHOW PROCESSLIST 里卡在 Updating 状态的线程明显增多。
常见误判是只看 WHERE 条件是否用到该字段。真正致命的是字段本身“动得勤”:比如 status、updated_at、version、score 这类每笔业务必改的字段。哪怕你只改一行、只改一个字段,只要它在索引里,开销就逃不掉。
怎么快速识别该删哪个索引
别猜,用数据说话。先执行:
SHOW INDEX FROM t;
重点标出满足以下全部条件的索引:
- 非主键、非唯一约束(
Non_unique = 1) - 字段更新频率 ≥ 1 次/秒(可通过应用日志或
performance_schema.events_statements_summary_by_digest估算) - 该字段在
WHERE中几乎不用(查慢查询日志或EXPLAIN结果,确认无type=ref或range依赖它)
临时验证手段不是设 INVISIBLE——那只是骗优化器,InnoDB 依然照常维护。正确做法是:
- 备份后执行
ALTER TABLE t DROP INDEX idx_status - 用真实流量压测 5 分钟,观察
sys.schema_table_statistics中updates延迟是否下降 ≥30% - 如果回升明显,说明这就是瓶颈索引,别犹豫,删掉
复合索引里放更新字段有多危险
联合索引不是“多个字段堆一起”,而是按顺序构建一棵树。只要其中任意字段被更新,整条索引项就得重写。尤其当高频变动字段放在靠前位置时,问题更严重:
-
(updated_at, user_id):每次时间戳变,整条索引全重写,等价于每行更新都引发页分裂 -
(status, created_at):哪怕created_at从不更新,只要status变,整个索引节点都要刷新 - 字段数超 3 个(如
(user_id, status, updated_at)):索引体积大 + 更新频次高 = 写放大成倍放大
安全做法是把稳定字段放最左,比如 (user_id, created_at),再单独为 status 加索引(仅当读远多于写且无更好过滤条件时);或者把变动字段拆解,例如用生成列 updated_day DATE AS (DATE(updated_at)),再建 INDEX idx_updated_day ON t (updated_day)。
覆盖索引在高频更新场景下可能适得其反
覆盖索引确实能省回表,但代价是索引更大、更新更重。对日均百万级 UPDATE 的表,它往往让写性能雪上加霜:
- 检查
performance_schema.table_io_waits_summary_by_index_usage,确认该索引的COUNT_READ是COUNT_WRITE的 10 倍以上才值得保留 - 避免用覆盖索引去加速
UPDATE ... SET amount = amount + 1 WHERE order_id = ?这类操作——索引里存了amount,每次加法都得同步更新索引 - 真要查得快,优先考虑冗余字段 + 应用层双写,比如在
users表里存total_points并建索引,而不是在日志表user_points_log的change_amount上硬扛索引
最常被忽略的一点:索引维护成本不是静态的。随着数据量增长和更新模式变化,昨天安全的索引,今天可能已成瓶颈。定期(建议每月)用 ANALYZE TABLE 更新统计信息,并结合 performance_schema 回溯索引实际读写比,比任何“最佳实践清单”都管用。











