UPDATE WHERE IN 在亿级表上会引发全表扫描,应改用 INNER JOIN 或分批更新(每次2000–5000行、显式事务、主键范围推进),并确保 WHERE 条件字段有合适索引。

UPDATE WHERE IN 走不了索引,别硬刚
直接写 UPDATE t SET status = 'done' WHERE id IN (1,2,3,...10000) 在亿级表上基本等于自毁——MySQL 会放弃使用 id 索引,触发全表扫描。这不是“慢”,是根本不可用。原因在于优化器对 IN 列表的代价估算失真,尤其当列表长度动态变化时。
替代方案不是换语法,而是绕开它:
- 用
INNER JOIN显式构造驱动表:UPDATE t INNER JOIN (SELECT 1 AS id UNION ALL SELECT 2 UNION ALL ...) AS tmp ON t.id = tmp.id SET t.status = 'done',确保t.id有索引且查询计划走ref - 若 ID 来源是另一张表,直接
UPDATE t INNER JOIN other_table o ON t.id = o.id SET t.status = 'done',比WHERE id IN (SELECT id FROM other_table)快一个数量级 - 禁止在
IN里拼接上万条值——应用层切分后循环执行,但单次不超过 500 条,否则网络包和解析开销反超收益
分批更新必须带显式事务 + 主键推进
百万行以上更新不加事务控制,失败时无法定位断点;不分批推进,锁表时间长、日志暴涨、主从延迟飙升。关键不是“分多少”,而是“怎么分”。
MySQL 下安全做法:
- 每次只处理固定范围:
UPDATE t SET status = 'done' WHERE id BETWEEN @start AND @end AND status = 'pending',@start和@end由外部计算好传入,避免子查询依赖 - 严禁用
LIMIT在UPDATE中直接分页(如UPDATE ... LIMIT 1000),它不保证顺序,高并发下会漏行或重复 - 每批必须包裹
BEGIN; ... ; COMMIT;,并在存储过程中用DECLARE CONTINUE HANDLER FOR SQLEXCEPTION捕获异常后ROLLBACK - 批次大小建议 2000–5000 行:太小(如 100)事务开销占比过高;太大(如 20000)易触发锁等待超时(
Lock wait timeout exceeded)
复合索引没建对,分批也白搭
分批语句跑得慢,90% 是因为 WHERE 条件字段没走索引。比如 UPDATE t SET x=1 WHERE status='pending' AND create_time ,只给 <code>status 建单列索引毫无意义。
必须按查询模式建复合索引:
- 把等值过滤字段放前面:
status是固定值,create_time是范围,索引应为(status, create_time),不是反过来 - 如果更新还依赖
id排序推进,索引要包含id或单独建(status, create_time, id),否则ORDER BY id会触发 filesort - 执行前务必用
EXPLAIN FORMAT=TREE看是否用了索引、是否出现Using where; Using index,而不是Using where; Using index condition后面还跟Using filesort
存储过程里别信“自动提交”
MySQL 存储过程中,默认是自动提交每条语句的。这意味着你写个循环做 100 批更新,每批都独立提交——日志写满、IOPS 拉满、主从延迟爆炸。
真正可控的做法只有两条路:
- 关掉自动提交:
SET autocommit = 0;,然后在循环外BEGIN,每批执行后不COMMIT,最后统一COMMIT—— 但风险极高,一旦中断整批回滚,且锁持有时间拉满 - 更推荐:每批自己
BEGIN→ 执行 →COMMIT/ROLLBACK,靠HANDLER保底。重点是:存储过程开头加SET SESSION innodb_lock_wait_timeout = 30;,防止某批卡死拖垮整个流程 - 别在存储过程里调用
SLEEP()控制节奏——它锁住连接不释放,连接池会快速耗尽;节奏控制交给应用层或调度系统
ERROR 1205 (40001): Deadlock found when trying to get lock。

















