结论:不推荐用触发器审计存储过程,应显式在存储过程中写日志。因触发器仅响应DML操作,无法关联调用来源;手动日志需记录proc_name、executed_by、时间、参数等,并避免事务回滚影响、高频插入及性能瓶颈。

直接说结论:在存储过程中做审计跟踪,不推荐用触发器自动捕获执行行为;真正可控、可落地的方式是“在存储过程内部显式写日志”,配合临时表或审计表手动记录关键字段。
为什么不能依赖触发器审计存储过程调用
触发器(TRIGGER)只响应 INSERT/UPDATE/DELETE 等 DML 语句对表的变更,它完全不知道谁、什么时候、以什么参数调用了哪个存储过程。即使你在目标表上建了 AFTER INSERT 触发器,也只能记录“某条数据被插入了”,但无法关联到是 usp_CreateOrder 还是 usp_ImportBatch 调用的——这两者可能都操作同一张 Orders 表。
常见错误现象包括:
- 误以为给
audit_log表加触发器就能反向追踪调用链,结果日志里只有空的SESSION_ID和GETDATE(),找不到源头存储过程名 - 在多个存储过程里反复更新同一张业务表,触发器日志混杂,无法区分归属
- 触发器嵌套调用导致递归写日志,甚至死锁
在存储过程内手动记录审计日志的实操要点
这是最直接、最可靠的方式:每个需要审计的存储过程,在关键逻辑前后主动插入一条记录到 audit_log 表。不是“拦截”,而是“声明式上报”。
必须注意以下几点:
-
audit_log表至少包含:proc_name(SYSOBJECTS.name或硬编码字符串)、executed_by(用SUSER_SNAME())、start_time/end_time、params_json(建议用CONCAT拼接或FOR JSON序列化)、row_count(@@ROWCOUNT)、error_message(ERROR_MESSAGE()) - 不要在事务主体中直接
INSERT INTO audit_log,否则失败会回滚审计记录;应改用EXEC sp_executesql+SET XACT_ABORT OFF,或把日志写入非事务表(如带WITH (TABLOCK)的独立日志表) - 避免在循环体里高频写审计日志;如需逐行记录,先缓存到表变量或临时表(
#audit_temp),最后批量INSERT - 参数记录别用
sp_help或元数据查,直接拼字符串——CONCAT('@p1=', ISNULL(CAST(@p1 AS VARCHAR(100)), 'NULL'), ', @p2=', @p2)
用临时表暂存审计上下文再统一落库
适用于批量操作类存储过程(比如导入、同步、清洗),需要在单次执行中记录数十甚至数百条子动作,但又不想每次插都走磁盘 I/O。
典型做法:
- 开头声明
CREATE TABLE #audit_batch (id INT IDENTITY, step VARCHAR(50), target_table VARCHAR(64), rows_affected INT, ts DATETIME2) - 每完成一个子步骤(如“更新客户状态”、“同步地址信息”),就
INSERT INTO #audit_batch一行 - 主逻辑结束后,用
INSERT INTO audit_log (...) SELECT ... FROM #audit_batch一次性提交 - 注意:临时表不跨连接,所以不能用于跨存储过程的链路追踪;若需链路级审计,得靠
CONTEXT_INFO()或传入唯一@trace_id参数贯穿全程
容易被忽略的兼容性与性能细节
很多人写了日志但查不到、或者上线后发现 CPU 突增,问题往往出在这些地方:
-
audit_log表没建索引,按proc_name或executed_by查询时全表扫描;至少要有(proc_name, created_at)复合索引 - 用
GETDATE()记时间,但在高并发下精度不够;生产环境建议用SYSDATETIME()或SYSDATETIMEOFFSET() - 把大对象(如 XML 参数、JSON 入参)原样存进
NVARCHAR(MAX)字段,导致日志表迅速膨胀;应做长度截断(LEFT(@params, 2000))或异步压缩归档 - SQL Server 2016+ 可考虑用内存优化表(
MEMORY_OPTIMIZED = ON)存临时审计缓冲,但要注意不支持FOREIGN KEY和某些函数
真正难的不是写那几行 INSERT,而是让日志能被快速检索、不拖慢主流程、且在故障时仍保留关键上下文——这三点决定了审计是否真的可用。










