mysql中通过old关键字获取更新前旧数据,仅在before/after update和delete触发器中可用且只读;字段名须严格匹配原表定义,含空格或关键字需用反引号包裹,insert中访问会报错。

触发器里怎么拿到更新前的旧数据
MySQL 的 BEFORE UPDATE 触发器能访问 OLD 关键字,它代表行更新前的完整快照。PostgreSQL 用 OLD 同样有效,但必须在 FOR EACH ROW 级别定义;SQL Server 则依赖 deleted 临时表。别试图在 AFTER UPDATE 里读原记录——那时 OLD 已不可用,deleted 表也只在触发器执行期间存在。
常见错误:写成 AFTER UPDATE 却还查原表,结果读到的是新值,备份内容和当前记录一模一样。
-
OLD是只读的,不能赋值或修改 - 字段名必须和原表完全一致,大小写敏感(尤其 PostgreSQL)
- 如果原表有自增主键,
OLD.id是更新前的值,不是新生成的
备份表结构要和原表保持严格一致吗
不需要完全一致,但关键字段类型、长度、是否允许 NULL 必须匹配,否则插入会失败。比如原表 content 是 TEXT,备份表对应字段要是 TEXT 或更大容量类型(如 MEDIUMTEXT),但不能是 VARCHAR(255)。
建议备份表额外加三个字段:backup_at(TIMESTAMP DEFAULT CURRENT_TIMESTAMP)、updated_by(可存触发器里获取的 USER() 或上下文变量)、original_id(存 OLD.id,方便反查)。
- 避免在备份表加主键或唯一约束——同一记录可能被多次更新,产生多条备份
- 不要给备份表加外键,否则删除原表时会报错或级联删掉历史
- 如果原表用了 JSON 字段,MySQL 5.7+ 备份表也得用
JSON类型,不能用TEXT存储后丢失校验
MySQL 触发器插入备份的典型写法
以用户表 users 为例,备份到 users_backup:
CREATE TRIGGER users_backup_before_update BEFORE UPDATE ON users FOR EACH ROW INSERT INTO users_backup SELECT OLD.*, NOW(), USER(), OLD.id;
注意这里没写列名列表——前提是 users_backup 字段顺序、数量、类型和 SELECT OLD.* 完全对齐。更稳妥的做法是显式列出:
INSERT INTO users_backup (id, name, email, status, backup_at, updated_by, original_id) VALUES (OLD.id, OLD.name, OLD.email, OLD.status, NOW(), USER(), OLD.id);
-
OLD.*在 MySQL 中可用,但 PostgreSQL 不支持,必须拆开写 - MySQL 8.0+ 支持在触发器里调用存储函数,但不建议在其中做复杂逻辑或远程调用,会拖慢主更新
- 如果备份表有
auto_increment主键,插入时别给它赋值,否则可能冲突
并发更新下备份丢失或重复的风险
触发器本身是事务内执行的,所以备份插入和主表更新要么一起成功,要么一起回滚。但真正危险的是高并发场景下的「幻读」式误判:比如两个事务同时更新同一行,第二个事务的 OLD 其实是第一个事务提交后的新值,导致备份漏掉中间态。
这不是触发器缺陷,而是业务层没控制好并发策略。若需精确捕获每次变更,得配合应用层加行锁(SELECT ... FOR UPDATE)或用乐观锁版本号。
- 别依赖触发器实现审计合规要求——它不记录谁发起的请求、IP、HTTP 参数等上下文
- 备份表数据量增长快,记得定期归档(如按月分区)或加清理任务
- 某些 ORM 框架(如 Django)批量更新(
QuerySet.update())会绕过模型层的 save 钩子,但无法绕过数据库触发器——这点反而成了优势
触发器备份看似自动,实际对字段映射、并发语义、运维成本都很敏感。上线前务必用真实更新流量压测,观察备份延迟和锁等待情况。











