必须用before update,因为只有它能在数据写入前拦截非法更新;after触发器执行时变更已落盘,无法真正阻止,mysql/pg中rollback或raise均无效,sql server虽支持after rollback但限制严苛且易引发死锁。

不能靠触发器“强制递增”,只能用 BEFORE UPDATE 拦截值变小的更新;一旦数据已写入,AFTER UPDATE 就完全失效。
为什么必须用 BEFORE UPDATE 而不是 AFTER
MySQL 和 PostgreSQL 的 AFTER UPDATE 触发器在语句执行完才触发,此时 NEW.value 已落盘——哪怕你立刻 ROLLBACK 或 RAISE,原更新也已生效。只有 BEFORE UPDATE 能在写入前读取 OLD.value 和 NEW.value 做比较,并用 SIGNAL(MySQL)或 RAISE EXCEPTION(PG)中断整个语句。
- SQL Server 是个例外:它允许在
AFTER UPDATE中ROLLBACK TRANSACTION,但前提是没开启递归且事务未提交 - 别信“在 AFTER 里 UPDATE 回去”的做法——这会引发二次触发、死锁或无限循环
- 所有数据库中,
BEFORE是唯一能真正阻止非法值写入的时机
如何安全比较 NEW 和 OLD 值(尤其含 NULL)
直接写 NEW.counter 在遇到 <code>NULL 时会返回 UNKNOWN,导致判断失效。MySQL 8.0.22+ 可用 IS DISTINCT FROM,但低版本和兼容性要求高的场景得手动判空:
- MySQL / SQL Server:
(NEW.counter IS NULL) != (OLD.counter IS NULL) OR NEW.counter - PostgreSQL:
NEW.counter IS DISTINCT FROM OLD.counter AND (NEW.counter (注意 NULL 视为最小值) - 别用
COALESCE(NEW.counter, 0)简单替换——若业务中 0 是合法值,就会掩盖真实越界
常见误判场景与绕过风险
表面看只是拦“变小”,但实际部署时容易漏掉这些点:
-
UPDATE t SET counter = counter - 1 WHERE id = 123这种自运算语句,OLD.counter是旧值,NEW.counter是计算后结果,比较逻辑依然有效 - 批量更新如
UPDATE t SET counter = 100 WHERE status = 'done',触发器逐行执行,每行都校验,没问题 - 但若应用层用
LOAD DATA INFILE或INSERT ... ON DUPLICATE KEY UPDATE,触发器仍生效——不过性能可能骤降,上线前必须压测 - 最大风险不在 SQL 层:只要有一个直连账号拥有
UPDATE权限且没被触发器覆盖(比如运维用 root 直连),防护就形同虚设
生产环境必须加的兜底措施
单靠触发器拦不住所有情况,尤其是跨服务、ETL 工具或旧脚本直连的场景:
- 权限隔离:对非必要账号
REVOKE UPDATE(counter)(PostgreSQL/SQL Server 支持列级权限,MySQL 5.7+ 也支持) - 白名单识别:在触发器里检查
CURRENT_USER()或application_name(PG),只对可信来源放行特殊操作(如重置计数器) - 监控告警:捕获
SIGNAL报错日志,统计拦截频次——异常飙升说明有服务在暴力重试或逻辑出错 - 别忘了
INSERT:首次插入时OLD.counter不存在,需额外判断NEW.counter 或是否符合起始规则
真正难的不是写那几行比较逻辑,而是确保所有数据写入口——包括那个三年前写的 Python 脚本、DBA 的临时查询窗口、甚至 BI 工具的“刷新数据”按钮——都跑在同一个触发器约束之下。











