百万行update必须分批处理,使用显式事务、order by游标、临时表join替代子查询,并确保where条件命中联合索引,同时禁用非必要约束并全程监控执行计划与锁状态。

直接在存储过程中对百万行执行单次 UPDATE,基本等于给数据库“上刑”——锁升级、日志爆满、超时断连、客户端卡死都是常态。真正能跑通的方案,必须用分段 + 批量 + 显式事务控制把大操作切成可控小块。
MySQL 存储过程里别用 LIMIT i,1 分页取数
这是最常踩的坑:用 LIMIT i,1 在循环中逐条取 id,会导致跳过数据甚至死循环。MySQL 的 LIMIT offset,row_count 在并发写入或非唯一排序下不保证稳定性,尤其当源表有删改时,i 递增后可能查不到新行或重复取同一行。
- 声明游标时必须带明确
ORDER BY id(不能省),避免无序扫描 - 用
DECLARE CONTINUE HANDLER FOR NOT FOUND捕获游标结束,而不是靠硬算总行数 - 每次
FETCH后立即处理并更新,不缓存中间变量 - 别在循环里嵌套
SELECT ... FOR UPDATE,它会放大锁冲突
每批必须显式事务 + 错误捕获,不能依赖自动提交
没加事务控制或错误处理,失败时根本不知道卡在哪一批。MySQL 存储过程默认 autocommit = ON,每条语句都是独立事务,redo 刷盘、锁校验、约束检查全来一遍,吞吐极低。
- 开头加
SET autocommit = 0,结尾显式COMMIT - 每 1000–5000 行执行一次
COMMIT(视单行大小和innodb_log_file_size调整) - 用
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION捕获异常,ROLLBACK并记录日志 - 批次大小不是越小越稳:
500行太碎,网络和事务开销反升;10000行又容易触发Lock wait timeout exceeded
UPDATE 本身要避开子查询嵌套,优先走 JOIN + 临时表
像 UPDATE A SET col = (SELECT x FROM B WHERE ...) 这种写法,在存储过程中每执行一次都会触发一次子查询,N 行就是 N 次全表/索引扫描,性能雪崩。
- 开头建
CREATE TEMPORARY TABLE temp_join (...),一次性载入所需字段 - 主
UPDATE改成UPDATE A JOIN temp_join ON A.id = temp_join.a_id SET A.col = temp_join.val - 临时表自动随会话销毁,无需手动
DROP -
WHERE条件必须命中联合索引,否则再好的存储过程逻辑也救不了全表扫描——先用EXPLAIN验证核心SELECT是否走ref或range,别信“写了存储过程就快”
真正影响速度的往往不是存储过程,而是 WHERE 条件有没有走索引
即使你把逻辑全写进存储过程,如果 WHERE pid = ? AND status = ? 这类条件没命中索引,照样全表扫描。而存储过程会让这种问题更隐蔽——你以为是“逻辑慢”,其实是“扫描慢”。
- 复合条件优先建联合索引,顺序按等值列在前、范围列在后(如
(pid, status, busi_id)) - 避免在
WHERE中对字段做函数操作,比如WHERE DATE(create_time) = '2026-08-01'会失效索引 - 批量更新前先禁用非必要约束:
SET FOREIGN_KEY_CHECKS = 0,完成后恢复
最容易被忽略的点是:事务边界和索引设计必须同步验证。光写对存储过程结构不够,EXPLAIN 看执行计划、SHOW ENGINE INNODB STATUS 查锁等待、slow_query_log 抓实际耗时——三者缺一不可。










