mysql update无法依赖order by控制加锁顺序,必须在应用层显式排序id后执行;select for update与update须同处一事务;in子句id应数值升序且主键为聚簇索引;大批量更新推荐用带主键的临时表+join替代in。

UPDATE语句里不能直接用ORDER BY排序
MySQL原生UPDATE在8.0.19之前完全不支持ORDER BY,即使新版允许,也仅限单表等值更新场景,对JOIN、子查询或IN批量更新无效。指望SQL自己排序是行不通的——InnoDB加锁顺序由内部扫描路径决定,而这个路径受索引选择、统计信息、优化器策略影响,不可控。
真正能干预加锁顺序的,只有应用层:先查出ID,显式排序,再按序执行或构造有序IN子句。否则两个事务分别执行UPDATE ... WHERE id IN (5,100,2)和UPDATE ... WHERE id IN (2,5,100),极大概率触发死锁。
SELECT FOR UPDATE必须和UPDATE在同一个事务里排序
常见错误是分两步走:先SELECT id FROM t WHERE ... ORDER BY id FOR UPDATE拿到ID列表,再在另一个事务里UPDATE t SET ... WHERE id IN (...)。这会导致锁提前释放,后续UPDATE变成无锁竞争,不仅没防住死锁,还可能引发数据覆盖。
- 必须把
SELECT ... FOR UPDATE和后续UPDATE放在同一个BEGIN ... COMMIT块内 - 中间不能有网络IO、日志打印、HTTP调用等非数据库操作,否则事务悬停,锁被长期占用
- 如果用ORM(如MyBatis、Django ORM),要确认它不会自动拆开这两个操作——有些框架会把
SELECT FOR UPDATE结果转成对象后才进事务,已掉坑
IN子句里的ID顺序≠加锁顺序,但可以间接控制
WHERE id IN (1,3,2)本身不保证InnoDB按这个顺序加锁;但如果你确保IN里的ID是升序排列,且id是主键(聚簇索引),InnoDB在使用主键索引查找时,通常会按物理存储顺序扫描,从而自然形成一致加锁流。这不是SQL标准,而是InnoDB实现细节,但足够可靠。
容易踩的坑:
- 用字符串比较排序ID(比如
['10', '2', '100']按字典序排成['10', '100', '2']),必须转为数值再sort() - ORM自动重排
IN参数(某些PHP PDO驱动、旧版JDBC会按绑定变量顺序重排),得实测或关掉预处理 - UUID主键即使排序了,物理存储仍是离散的,锁竞争依然存在,此时应强制分批(如每次≤50条)并加
SLEEP(0.01)错峰
大批量更新优先用临时表+JOIN替代IN
当ID数量超过几百、或需要关联其他业务字段(比如新状态来自另一张配置表)时,拼IN既难维护又易超长(max_allowed_packet限制),还无法验证加锁顺序。临时表方案更健壮:
CREATE TEMPORARY TABLE tmp_update (id BIGINT PRIMARY KEY, new_status TINYINT); INSERT INTO tmp_update VALUES (1,'paid'), (2,'shipped'), (3,'canceled'); UPDATE orders o JOIN tmp_update t ON o.id = t.id SET o.status = t.new_status;
关键点:
- 临时表必须定义
PRIMARY KEY(或唯一索引),否则JOIN时InnoDB可能走嵌套循环+全表扫描,锁范围爆炸 -
INSERT语句本身不强制物理顺序,但只要SELECT ... ORDER BY id后插入,或建表后ALTER TABLE tmp_update ORDER BY id,就能让JOIN扫描按主键序进行 - 该方式天然规避应用层排序逻辑,适合Go/Python等无强类型约束的语言,也方便审计和回滚
最易被忽略的是:临时表方案看似绕路,实则把“排序”这件事从应用层移交给了MySQL执行器,而执行器对主键顺序的利用比任何手写sort()都更底层、更稳定——尤其当业务代码跑在多语言、多版本环境中时。











