MySQL触发器需分INSERT/UPDATE/DELETE单独编写:INSERT用BEFORE记录NEW,UPDATE用OLD/NEW比对字段,DELETE用BEFORE记录OLD;审计表须含table_name、row_id、operation、changed_at、changed_by(应用层传入)。

MySQL 触发器怎么捕获 INSERT/UPDATE/DELETE 的旧值和新值
触发器本身不提供统一的“变更前后快照”接口,INSERT 没有 OLD,DELETE 没有 NEW,只有 UPDATE 同时可用两者。硬写成一个通用触发器会出错或漏数据。
实操建议:
- 为每种操作单独建触发器:用
BEFORE INSERT记录新行,BEFORE UPDATE记录OLD.*和NEW.*差异字段(别全存),BEFORE DELETE记录OLD.* - 审计表字段要包含:
table_name、row_id(主键值)、operation('I'/'U'/'D')、changed_at、changed_by(需从应用层传入,触发器拿不到 HTTP Header 或 session) - 别在触发器里调
CURRENT_USER()当操作人——它返回数据库连接用户,不是业务用户 -
UPDATE触发器中,用IF OLD.col1 != NEW.col1 THEN ... END IF;判断是否真修改,避免无意义日志
PostgreSQL 的 pg_trigger_depth() 和 TG_OP 怎么安全用
PostgreSQL 触发器函数里,TG_OP 是唯一可靠的操作类型标识符,但容易误判嵌套场景:比如审计表自己被写入,又触发了另一个同名触发器,形成无限递归。
实操建议:
- 必须检查
pg_trigger_depth() = 0才执行审计逻辑,否则跳过——这是防递归的最低成本手段 - 不要依赖
current_setting('app.user_id', true)做权限或归属判断,除非你已在连接初始化时显式设过(如SET app.user_id = '123'),否则返回 NULL - 审计表主键建议用
serial+created_at DEFAULT NOW(),别用UUID——高并发下gen_random_uuid()有性能抖动 - 如果审计字段含
JSONB(比如存差异),用to_jsonb(NEW.*) - 'updated_at'快速剔除时间戳类字段,比手写字段列表更耐改
SQL Server 的 INSERTED 和 DELETED 临时表为什么查不到数据
这两个是“伪表”,只在触发器作用域内可见,且内容取决于操作类型:INSERT 时 DELETED 为空,DELETE 时 INSERTED 为空,UPDATE 两者都有。直接 SELECT * FROM INSERTED 在非触发器环境会报 Invalid object name 'INSERTED'。
实操建议:
- 触发器开头先
IF NOT EXISTS (SELECT 1 FROM INSERTED) RETURN,避免空插入触发冗余逻辑 - 别对
INSERTED/DELETED做JOIN以外的 DML(比如UPDATE INSERTED),语法不支持 - 记录变更时,用
COLUMNS_UPDATED()判断哪些列被改——它返回位图,比逐字段比较快,但要注意字节序和字段顺序 - 审计表字段类型尽量匹配源表,尤其
datetime2(7)别写成datetime,否则毫秒精度丢失
触发器审计的性能坑:什么时候该停手
单条 DML 触发一次审计写入看似简单,但真实业务中常遇到批量导入、定时同步、报表更新等场景,一条 INSERT INTO ... SELECT 可能带几万行——这时每个行都进触发器,审计表 I/O 瞬间翻倍,主表写入也卡住。
实操建议:
- 对已知批量操作,在事务开始前关掉触发器:
DISABLE TRIGGER audit_trg ON target_table(MySQL 不支持,PG/SQL Server 支持) - 审计表加
ON COMMIT REFRESH物化视图(PG)或走异步队列(如写 Kafka,由消费者落库),把强一致性降级为最终一致 - 如果源表有
JSON或TEXT大字段,审计表对应字段设为NULL或只存LENGTH(),别全量镜像——磁盘和复制压力会指数增长 - 上线前必做:用
EXPLAIN ANALYZE跑一遍带触发器的UPDATE,看执行计划里有没有Seq Scan扫审计表(说明触发器里写了子查询关联审计表)
最麻烦的从来不是怎么记,而是怎么在记和不拖慢之间找那个刚好能接受的点——比如只记关键字段、只记敏感操作、只记特定用户组。留个开关比硬编码更实际。

















