千万级数据更新或删除必须分批次提交,否则会锁表、拖垮主从、触发超时;应按主键范围切片、显式COMMIT、每批500–2000行,并避免隐式转换、OFFSET分页等危险写法。

直接结论:千万级数据的更新或删除,不能用单条 UPDATE 或 DELETE 语句包干,必须分批次提交,否则锁住大量行、拖垮主从、甚至触发 Lock wait timeout exceeded。
为什么大事务会卡死线上服务?
InnoDB 的行锁不是“逐行加锁后立刻释放”,而是等到整个事务 COMMIT 才统一释放。一次更新 50 万行,意味着:
- 这 50 万行在事务期间全部被加锁(包括间隙锁),其他写操作一碰就等超时
- undo log 持续膨胀,可能占满
ibdata1或触发磁盘 IO 飙升 - binlog 写入巨大,主从复制延迟拉长,严重时从库追不上
- 事务时间越长,被 kill 或崩溃回滚的风险越高——回滚本身也可能卡住数小时
分批更新的核心控制点:主键范围 + 显式 COMMIT
最稳妥的方式是按主键(最好是自增 id)切片,每次只处理固定数量的连续 ID 段。关键不是“怎么写循环”,而是确保每批都真正提交并释放锁:
- 用
WHERE id BETWEEN @start AND @end,避免ORDER BY ... LIMIT引发的非确定性扫描 - 每批执行完必须跟
COMMIT;不要依赖自动提交,显式写出来更可控 - 批次大小建议 500–2000 行,具体看单行数据宽度和服务器负载,先试 1000 行观察锁等待和慢日志
- 如果主键不连续(比如有大量删除),改用
WHERE id > @last_id ORDER BY id LIMIT 2000更安全
别踩这些坑:OFFSET 分页、子查询、隐式类型转换
以下写法看着像分批,实际仍可能全表扫描或锁全表:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
UPDATE t SET status=1 WHERE id IN (SELECT id FROM t WHERE create_time —— 子查询没索引支撑时,外层 UPDATE 可能锁住全表匹配行-
UPDATE t SET status=1 WHERE id > 1000000 ORDER BY id LIMIT 1000——ORDER BY+LIMIT在无覆盖索引时可能触发 filesort,锁范围不可控 -
WHERE phone = 13812345678(phone是VARCHAR)—— 隐式类型转换导致索引失效,退化为全表扫描+全表加锁
真正安全的底线操作:先查后更,带索引条件闭环
最不容易出错的模式是“查 ID 列表 → 按 ID 更新 → 提交”,哪怕多一次查询也值得:
SELECT id FROM t WHERE create_time 1000000 ORDER BY id LIMIT 1000;
拿到结果后,用 WHERE id IN (1000001,1000002,...) 更新。这样能确保:
- 每次只锁明确列出的那些行,无间隙锁扩散风险
- 不依赖执行计划是否走索引,ID 列表本身就是精确命中
- 可随时中断、跳过已处理 ID,便于人工干预
复杂点在于业务逻辑耦合强时难以抽离 ID 查询;最容易被忽略的是忘记在 WHERE 条件里复用原始筛选字段(比如只用 id IN (...) 却漏了 create_time 校验),导致误更新历史数据。










