执行UPDATE前必须四步并行:备份、加事务、用WHERE、启用sql_safe_updates;先SELECT预览、复用WHERE、包裹事务、检查ROW_COUNT()、开启safe-updates、大表加version防覆盖。

必须提前备份、加事务、用 WHERE 条件、启用 sql_safe_updates,四者缺一不可——漏掉任意一个,就可能让一条 UPDATE 变成“数据核弹”。
执行 UPDATE 前必须先做 SELECT 预览
这是最廉价也最有效的防线。很多人跳过这步,直接写 UPDATE,结果发现改错了才后悔。
- 先跑一遍
SELECT *或SELECT COUNT(*),确认目标行数和内容是否符合预期 - WHERE 条件要完全复用,不能手打两遍——复制粘贴,避免拼写/大小写/引号不一致
- 如果表很大,
SELECT加LIMIT 10快速抽样,比盲目执行更可靠 - Navicat / DBeaver 等工具支持“右键生成 WHERE 条件”,比手敲更少出错
所有 UPDATE 必须包裹在事务里,并手动控制提交
MySQL 默认 AUTOCOMMIT = 1,意味着每条语句单独提交,一旦出错无法回滚。
- 开头加
SET AUTOCOMMIT = 0;,或显式写START TRANSACTION; - 执行
UPDATE后,立刻查SELECT ROW_COUNT();确认影响行数是否合理 - 没问题再
COMMIT;;有问题立刻ROLLBACK;(注意:断开连接会自动回滚) - 别依赖 ORM 的“自动事务”——Spring 的
@Transactional只包方法,不保你手写的原生 SQL
强制开启 sql_safe_updates 并理解它拦什么、不拦什么
sql_safe_updates = 1 不是万能锁,但它是防止“全表 UPDATE”的第一道硬闸。
- 生效前提:当前会话未用
SUPER权限(如 root 登录会跳过),且语句是普通UPDATE/DELETE - 它会拦截:
UPDATE t SET a = 1;(无WHERE),或WHERE create_time > '2025-01-01'(条件列无索引) - 它不会拦截:
DROP TABLE、TRUNCATE、子查询形式的WHERE、跨库操作、应用直连未设该参数的连接 - 推荐方式:在
my.cnf的[mysql]段加safe-updates,让所有开发人员的命令行客户端默认启用
大表更新或高并发场景下,必须引入 version 字段防覆盖
当多线程/多服务同时更新同一行时,WHERE id = ? 不足以保证原子性,“后写入者覆盖前写入者”就是典型的丢失更新。
- 表中加
version INT DEFAULT 0字段,每次更新都带校验:UPDATE t SET status = 'done', version = version + 1 WHERE id = 123 AND version = 5 - 执行后必须检查返回影响行数:
if (rows == 0) throw new OptimisticLockException(); - 失败后不能重试旧值,要重新
SELECT拿最新version,再构造新语句 - ORM 用户注意:
@Version注解或@UpdateById方法只对框架生成 SQL 有效;手写 XML 或自定义 SQL 时,AND version = ?必须自己拼
真正危险的不是语法错误,而是“看起来执行成功了,其实什么都没改”或者“改了但没人知道改了多少”。ROW_COUNT() 的返回值、sql_safe_updates 的报错提示、事务的 ROLLBACK 能力,这些都不是可选项——它们是你在生产环境按下回车前,最后能抓住的几根绳子。

















