MySQL必须用BEFORE DELETE触发器确保日志与主删同事务;PG需先建RETURNS trigger函数并RETURN OLD;日志表须时区安全、联合索引、分区;Oracle优先用FGA避免mutating错误。

MySQL里用BEFORE DELETE触发器捕获被删行
触发器是唯一能在DELETE语句执行前拿到完整旧数据的方式。AFTER DELETE里OLD仍可用,但事务已开始提交,万一日志表写入失败,主删操作却成功了,就会丢日志。
实操建议:
- 必须用
BEFORE DELETE,确保日志写入和主删除在同一个事务里 - 日志表字段要覆盖被删表的主键+关键业务字段,别只记
id——比如删用户时没记email,后续查不到是谁被删 - 日志表引擎必须和主表一致(通常是
InnoDB),否则跨引擎事务可能出问题 - 触发器里别调用存储过程或外部服务,会拖慢删除、增加失败风险
CREATE TRIGGER user_delete_log BEFORE DELETE ON users FOR EACH ROW INSERT INTO user_delete_log (user_id, email, deleted_at, deleted_by) VALUES (OLD.id, OLD.email, NOW(), USER());
PostgreSQL中用触发器+函数记录DELETE操作
PG不支持直接在触发器体里写SQL,必须先建FUNCTION,再绑到触发器。常见错误是忘了RETURNS trigger或漏掉RETURN OLD,导致触发器报错中断删除。
实操建议:
- 函数必须声明
RETURNS trigger,结尾必须RETURN OLD(BEFORE触发器)或RETURN NULL(AFTER) - 用
current_setting('app.user_id', true)这类方式传上下文比硬编码USER()更可靠,尤其用连接池时 - 避免在触发器里做
SELECT查其他大表,容易锁表或拖慢主操作 - 日志表加
ON CONFLICT DO NOTHING防重复插入(比如重放binlog时)
CREATE OR REPLACE FUNCTION log_user_deletion() RETURNS TRIGGER AS $$ BEGIN INSERT INTO user_delete_log (user_id, email, deleted_at) VALUES (OLD.id, OLD.email, NOW()); RETURN OLD; END; $$ LANGUAGE plpgsql;
触发器日志表设计避坑点
日志表看着简单,但字段类型不对、索引缺失、没分区,半年后查一条记录要几秒,运维半夜被叫醒。
实操建议:
-
deleted_at用TIMESTAMP WITH TIME ZONE(PG)或TIMESTAMP(MySQL),别用DATETIME——时区混乱会导致日志时间对不上 - 主键别设
auto_increment,用(user_id, deleted_at)联合索引更实用;单列自增对查询毫无帮助 - 超过500万行必须按月分区(如
deleted_at),否则SELECT * FROM log WHERE deleted_at > '2024-01-01'全表扫 - 别把
OLD.*全转成JSON塞进一个TEXT字段——没法走索引,也查不了特定字段变化
Oracle里AUDIT + Fine Grained Auditing(FGA)比触发器更稳
Oracle触发器删记录时写日志,容易撞上ORA-04091: table is mutating错误——这是它自己限制,不是你代码写错了。
实操建议:
- 优先用
DBMS_FGA.ADD_POLICY配FGA策略,它绕过触发器机制,不触发mutating table限制 - FGA日志默认进
DBA_FGA_AUDIT_TRAIL,记得开AUDIT_TRAIL=DB,EXTENDED参数,否则只记动作不记OLD值 - 如果非要用触发器,得改用
PRAGMA AUTONOMOUS_TRANSACTION,但会脱离原事务——删失败了日志却写了,得靠定时任务对账 - 别在触发器里
INSERT到同实例另一库的表,跨DB链接不稳定,容易卡死主删流程
触发器日志最麻烦的不是写法,是“谁在删”和“为什么删”这两个信息根本不在OLD里——得靠应用层传上下文,或者提前在会话里设变量,这点很容易被忽略。










