before insert/update是唯一能真正阻止非法数据落盘的校验入口,因其在数据写入前执行,可通过signal/raise等中断事务;after触发器执行时数据已落盘,报错无法回滚主表变更,仅能事后清理。

BEFORE INSERT/UPDATE 是唯一能真正阻止非法数据落盘的校验入口。放在 AFTER 里查余额、比价格、抛错误,都晚了——数据已写入,SIGNAL 或 THROW 只能回滚事务,但调用方可能没捕获,用户看到的是“成功”,后台却在静默清理脏数据。
为什么必须用 BEFORE 而不是 AFTER?
核心就一条:校验目标是“不让错数据进来”,不是“进来后再打回去”。AFTER INSERT 触发时,INSERT 已执行完毕,哪怕你立刻 RAISERROR 或 THROW,那一行已经落在表里了。尤其在没有显式事务包裹的应用场景下,这个“回滚”甚至不会发生。
-
BEFORE INSERT:可读写NEW(MySQL/PostgreSQL)或:new(Oracle),适合补默认值、查关联状态、抛错中断 -
BEFORE UPDATE:OLD只读,NEW可改,必须同时检查两者——比如订单状态从'shipped'改为'cancelled',得先确认发货单是否已归档 -
AFTER DELETE:只能读OLD,适合做依赖检查(如“删客户前确认无未完成订单”),但不能阻止删除本身
跨表查询怎么写才不卡死?
别写 SELECT COUNT(*) > 0 FROM orders WHERE user_id = NEW.user_id。它会全表扫描、锁升级、批量导入时直接拖垮数据库。正确做法是用 EXISTS + 索引,且只查必要字段。
- 写法必须是:
EXISTS (SELECT 1 FROM orders WHERE user_id = NEW.user_id AND status = 'pending') -
user_id字段必须有索引;如果加了复合条件(如status = 'pending'),最好建覆盖索引:INDEX idx_user_status (user_id, status) - 禁止在子查询里用
ORDER BY或LIMIT——MySQL/PostgreSQL 都不支持,直接报错 - Oracle 中避免
SELECT ... FROM dual WHERE EXISTS (...)套壳,直接写IF EXISTS (...) THEN ...(需 PL/SQL 块)
抛错中断必须用对语法,否则等于没写
不同数据库的中断机制完全不同,混用会导致静默失效或语法报错。
- MySQL:
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户余额不足'—— 缺SIGNAL、缺SQLSTATE、字符串用双引号,全都不生效 - PostgreSQL:
RAISE EXCEPTION '用户余额不足'—— 写成RAISE NOTICE不会中断,只是打日志 - SQL Server:
THROW 50000, '用户余额不足', 1或RAISERROR('用户余额不足', 16, 1)—— 错误级别低于 11 不会终止事务 - Oracle:
raise_application_error(-20001, '用户余额不足')—— 错误码必须在 -20000 ~ -20999 范围内
最容易被忽略的三个硬伤
它们不会让你的触发器语法报错,但上线后一压测就崩,而且极难复现。
- 多行操作下误用标量子查询:
IF (SELECT status FROM users WHERE id = NEW.user_id) = 'blocked'在批量插入时会报“子查询返回多于一个值”,应改用EXISTS或提前用变量承接 - NULL 比较失效:
WHERE user_id = NULL永远为UNKNOWN,必须写成user_id IS NULL或user_id IS NOT NULL - 触发器里更新当前表:
UPDATE orders SET updated_at = NOW() WHERE id = NEW.id在 MySQL 中直接触发ERROR 1442,Oracle 和 SQL Server 同样禁止











