mysql和postgresql中old/new字段需按触发时机(before/after、insert/update/delete)正确访问,区分大小写且不可跨数据库直接移植,误用会导致报错或性能问题。
触发器里怎么拿到 old 和 new 的字段值
mysql 和 postgresql 都支持在行级触发器中用 old 和 new 引用修改前后的整行数据,但访问方式不同,不能直接当普通变量用。
常见错误是写成 OLD.username 却忘了当前数据库不支持点号语法(比如 PostgreSQL 在函数体里得用 OLD.username,但 MySQL 里必须用 OLD.username——等等,其实都支持,问题出在别处);更常踩的坑是:在 BEFORE DELETE 触发器里还去读 NEW,结果报错 NEW is not available in DELETE triggers。
- MySQL 中,
OLD和NEW是只读伪记录,字段名区分大小写,且必须和表定义完全一致(比如表字段是user_name,就不能写OLD.username) - PostgreSQL 中,
OLD/NEW是 RECORD 类型,在 PL/pgSQL 函数里可直接用点号访问,如OLD.id;但若触发器函数声明为RETURNS trigger,就必须返回NEW(INSERT/UPDATE)或OLD(DELETE),否则会中断操作 - SQLite 不支持
OLD/NEW伪记录,只能靠UPDATE ... SET col = old_col WHERE rowid = ?这类手动模拟,本质没真触发器上下文
BEFORE 和 AFTER 触发器对 OLD/NEW 的可用性差异
不是所有时机都能同时访问两个伪记录。核心规则就一条:哪个数据还存在,才能读哪个。
-
BEFORE INSERT:只有NEW可用(还没插入,自然没有旧值) -
BEFORE UPDATE:OLD和NEW都可用,且NEW可被修改(比如自动更新updated_at字段) -
BEFORE DELETE:只有OLD可用(要删的是它,没新行) -
AFTER类触发器中,OLD/NEW仍可读,但不能再改NEW(MySQL 报错Can't update table in stored function/trigger,PostgreSQL 要求函数返回值匹配)
典型误用:在 AFTER UPDATE 里试图给 NEW.status = 'processed' —— 这不会生效,语句已执行完,改了也白改。
跨数据库移植时 OLD/NEW 的兼容性陷阱
看着名字一样,实际行为差很远。最痛的点是字段类型隐式转换和 NULL 处理。
- MySQL 中,如果某字段定义为
NOT NULL但触发器里写了SET NEW.col = NULL,会直接报错中断;PostgreSQL 则允许你在BEFORE触发器里设NEW.col := NULL,哪怕列有NOT NULL约束——只要后续 INSERT/UPDATE 语句本身没违反即可 - PostgreSQL 的
OLD/NEW在触发器函数内是 RECORD 类型,不能直接参与比较(如IF OLD != NEW THEN),得逐字段比,或用ROW(OLD.*) IS DISTINCT FROM ROW(NEW.*) - MySQL 8.0+ 支持 JSON 函数,可以用
JSON_CONTAINS_PATH(JSON_OBJECT('a', OLD.a, 'b', OLD.b), 'one', '$.a')做动态字段检查;但老版本只能硬写字段列表
别指望一个触发器 SQL 脚本在 MySQL 和 PG 里直接跑通,连注释风格都要改——MySQL 用 --,PG 用 -- 或 /* */ 都行,但函数体里的字符串引号、转义规则全不一样。
审计日志场景下读取 OLD/NEW 的性能注意点
想用触发器记变更日志?小心查 OLD/NEW 本身不慢,但后续 INSERT 日志表可能拖垮主表性能。
- 避免在触发器里做复杂计算或远程调用(比如 HTTP 请求、加解密),MySQL 触发器不支持外部通信,PG 虽能用
dblink但会锁事务 - PostgreSQL 中,如果日志表和原表在同一个事务里 INSERT,且日志表有索引,每行变更都会多一次索引更新 —— 百万级更新可能卡死
- MySQL 里不要在触发器里 SELECT 其他大表(比如 JOIN 用户表查姓名),触发器执行期间会持有行锁,容易引发死锁
- 更稳的做法:只把关键字段(
OLD.id,NEW.status,USER())塞进日志,其余信息留到异步任务里补全
真正难的不是怎么取值,是怎么确保取了之后不影响主业务的原子性和响应时间。很多线上事故,都是因为以为 OLD.status 很轻量,结果触发器里顺手加了个子查询,就把订单表锁了两秒。










