UPDATE用了索引但更慢,根本不是“有没有索引”的问题,而是索引维护开销大:改索引列需重写整条索引项,触发页分裂、redo/undo日志写入;冗余索引、长字段全量索引、隐式转换等均加剧写负担,user_updates远高于user_seeks即为典型拖油瓶。

UPDATE 用了索引但反而更慢,根本不是“有没有索引”的问题
索引存在 ≠ UPDATE 快。真正拖慢的,是每次更新都要同步维护所有相关索引项——包括定位、删除旧条目、插入新条目、写 redo/undo log、可能触发页分裂。实测中,单表每秒 UPDATE 超过 50 次且目标字段在索引里时,QPS 下降 30%~60% 很常见。
你看到的 EXPLAIN 显示走了索引,只说明 WHERE 条件命中了索引;但 SET 部分只要改的是索引列(哪怕联合索引里只是第二列),整个索引项就得重写。这不是“用没用索引”,而是“用得多不多、写得多不多”。
哪些索引会让 UPDATE 变成“自虐式维护”
以下情况会让索引从加速器变成拖油瓶:
-
status字段被高频更新(如支付状态流转),却单独建了INDEX(status)—— 每次改 status,就要刷一遍这个索引树 - 已有
INDEX(status, created_at),又额外加了INDEX(status)—— 冗余索引导致双倍写开销 - 对
varchar(500)字段建了全量索引,但实际查询只匹配前 10 个字符 —— 索引体积大、更新成本高,却没带来额外收益 - WHERE 条件里用了
UPPER(email)或隐式转换(如user_id = 123但字段是VARCHAR)—— 看似走了索引,实则退化为扫描,还连带拖垮所有关联索引的更新路径
怎么验证是不是索引在拖慢 UPDATE
别猜,直接查数据和执行计划:
- 运行
SELECT * FROM sys.dm_db_index_usage_stats WHERE object_id = OBJECT_ID('orders')(SQL Server)或SELECT index_name, user_updates, user_seeks FROM information_schema.statistics WHERE table_name = 'orders'(MySQL)—— 如果某个索引的user_updates远高于user_seeks,说明它光被写、几乎不被读 - 在 MySQL 中打开慢日志,看该 UPDATE 的
Rows_examined是否远大于影响行数(比如只改 100 行,却扫了 50 万行)—— 这往往意味着 WHERE 没走对索引,或统计信息过期 - 用
SET STATISTICS XML ON(SQL Server)或EXPLAIN FORMAT=JSON(MySQL)看执行计划里是否有Wide关键字:没有,说明没触发批量索引更新,还在用低效的逐行窄计划
不删索引,还能怎么救 UPDATE 性能
真要保留索引(比如 status 是高频查询条件),又不想让写崩掉,得做减法:
- 把单列
INDEX(status)替换为覆盖查询的联合索引,例如INDEX(status, created_at, id),避免回表,同时减少索引总数 - 对长文本字段,改用前缀索引:
INDEX(content(10)),体积小、更新快、仍能支撑LIKE 'abc%'类查询 - 确认业务是否真的需要实时更新:能否把部分状态变更转为异步任务?或用写缓冲(如 Redis + 延迟双删)降低数据库直写压力
- 定期跑
ANALYZE TABLE orders(MySQL)或UPDATE STATISTICS orders(SQL Server)—— 统计不准时,优化器可能选错执行路径,让本可走 seek 的操作被迫 scan
最常被忽略的一点:联合索引里只要有一列被 UPDATE,整条索引记录就得重建。哪怕你只改了 updated_at,而它在 (status, updated_at) 的第二位,B+ 树叶节点照样要重写——这个代价,比你想象中更实在。

















