必须用自增id分段,因其单调递增、无重复、有索引、非空,而created_at等字段易因并发写入、时钟漂移、空值或重复导致漏更或重更;安全分段UPDATE需基于上轮最大id严格递进,带ORDER BY id和LIMIT,每批500–2000行为宜。

直接在生产表上跑百万级 UPDATE,大概率会卡死、锁表、拖垮主从同步。必须分段,且必须用自增 id 做游标——其他字段(比如时间、状态)几乎都不可靠。
为什么非得用自增 id 分段?
自增 id 是单调递增、无重复、有索引、不为空的天然游标。换成 created_at 或 updated_at,哪怕加了索引,也会因并发写入、时钟漂移、空值或重复值导致漏更新或重复更新。
-
WHERE created_at > '2025-01-01':如果该字段没索引,全表扫描;有索引但范围太大,照样锁几百上千万行 -
WHERE status = 0单独出现:优化器可能放弃索引,退化为全表扫描 - 先
SELECT MAX(id)再UPDATE:中间插入新记录,这批就永远漏掉
怎么写安全的分段 UPDATE 语句?
核心是原子性 + 边界严格递进。每次只靠上一轮最后更新的 id 值推进,不查、不估、不依赖外部状态。
- 语句必须带
ORDER BY id和LIMIT,否则 MySQL 可能跳过某些行 -
WHERE条件里必须同时包含id > ?和业务条件(如status = 0),不能只写后者 - 示例:
UPDATE user_order SET status = 1 WHERE id > 100000 AND status = 0 ORDER BY id LIMIT 1000 - 执行后立刻用
SELECT LAST_INSERT_ID()或客户端记录本次影响行中最大的id(比如 100999),下一轮WHERE id > 100999
每批多少行合适?
500–2000 行是实测较稳的区间。太小(如 100)会导致事务开销和网络往返激增;太大(如 5000+)容易触发锁升级或超时。
- 单事务超过 5 万行,基本会触发
Lock wait timeout exceeded - 超过 10 万行,主从延迟常突增至分钟级,
undo log膨胀严重 - 别在高峰期跑;更别在从库上直接执行——即使只读,也可能因元数据锁阻塞复制线程
容易被忽略的边界细节
真正出问题的,往往不是语法,而是边界处理的松动。
- 不要用
BETWEEN @min_id AND @min_id + @batch_size - 1:如果中间有DELETE或REPLACE,id不连续,就会跳过空洞 - 避免在
WHERE里套函数,比如DATE(created_at)—— 索引失效,直接扫全表 - 如果原表没有自增
id,别硬凑ROW_NUMBER()模拟——MySQL 8.0 才支持窗口函数,且无法在UPDATE中直接使用


















