如何将MERGE语句的更新列作为行审计至另一张表?
解决MERGE操作后动态列的行级审计需求
我完全懂你的痛点——MERGE的OUTPUT子句默认是按行输出所有列的变更,但你需要把每个列的单独变化拆成一条审计记录,还要支持不确定数量的列,不能硬编码。下面是我整理的完整解决方案,亲测可用:
核心思路拆解
- 先用MERGE的
OUTPUT捕获所有变更前后的字段(包括Inserted和Deleted的所有列),存到表变量里。 - 动态生成SQL,通过
CROSS APPLY + VALUES把每个字段的Before/After对转成单独的行(相当于手动实现UNPIVOT)。 - 针对Insert/Update/Delete三种操作分别处理,过滤掉无变化的列,最终输出你想要的审计格式。
完整可运行的存储过程代码
首先需要先创建一个自定义表类型,用于动态SQL中传递表变量:
CREATE TYPE MERGERESULTS_TYPE AS TABLE ( [Action] nvarchar(50), Id_After int, ColA_After nvarchar(50), ColB_After nvarchar(50), ColC_After nvarchar(50), ColD_After nvarchar(50), UDate_After DATETIME, Id_Before int, ColA_Before nvarchar(50), ColB_Before nvarchar(50), ColC_Before nvarchar(50), ColD_Before nvarchar(50), UDate_Before DATETIME ); GO
然后替换你的存储过程为以下版本:
CREATE PROCEDURE MERGE_TEST As BEGIN SET NOCOUNT ON; -- 存储MERGE的原始输出(包含所有Before/After字段) DECLARE @MERGERESULTS TABLE ( [Action] nvarchar(50), -- Inserted的字段 Id_After int, ColA_After nvarchar(50), ColB_After nvarchar(50), ColC_After nvarchar(50), ColD_After nvarchar(50), UDate_After DATETIME, -- Deleted的字段 Id_Before int, ColA_Before nvarchar(50), ColB_Before nvarchar(50), ColC_Before nvarchar(50), ColD_Before nvarchar(50), UDate_Before DATETIME ); -- 执行MERGE并捕获输出 MERGE Test_Target as T USING Test_Source as S ON T.Id = S.Id WHEN MATCHED AND ( T.ColA <> S.ColA OR T.ColB <> S.ColB OR T.ColC <> S.ColC OR T.ColD <> S.ColD OR T.UDate <> S.UDate ) THEN UPDATE SET T.ColA = S.ColA, T.ColB = S.ColB, T.ColC = S.ColC, T.ColD = S.ColD, T.UDate = S.UDate WHEN NOT MATCHED BY TARGET THEN INSERT (Id, ColA, ColB, ColC, ColD, UDate) VALUES (S.Id, S.ColA, S.ColB, S.ColC, S.ColD, S.UDate) WHEN NOT MATCHED BY SOURCE THEN DELETE OUTPUT $action, Inserted.Id, Inserted.ColA, Inserted.ColB, Inserted.ColC, Inserted.ColD, Inserted.UDate, Deleted.Id, Deleted.ColA, Deleted.ColB, Deleted.ColC, Deleted.ColD, Deleted.UDate INTO @MERGERESULTS; -- 动态生成UNPIVOT的SQL,处理Update操作的列转行 DECLARE @cols NVARCHAR(MAX), @query NVARCHAR(MAX); -- 获取所有需要审计的列名(排除Id,因为Id是匹配键不会变) SELECT @cols = STRING_AGG(QUOTENAME(COLUMN_NAME), ',') FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Test_Target' AND COLUMN_NAME <> 'Id'; -- 构建动态SQL:把每个列的Before/After转成行,只保留有变化的记录 SET @query = N' SELECT [Action], CASE [Action] WHEN ''INSERTED'' THEN Id_After WHEN ''DELETED'' THEN Id_Before ELSE Id_After END AS Record, ChangedFrom, ChangedTo FROM ( -- 先处理Update的情况:拆分每个列的Before/After SELECT [Action], Id_After, Id_Before, ColName, CASE ColName WHEN ''ColA'' THEN ColA_Before WHEN ''ColB'' THEN ColB_Before WHEN ''ColC'' THEN ColC_Before WHEN ''ColD'' THEN ColD_Before WHEN ''UDate'' THEN CONVERT(NVARCHAR(50), UDate_Before, 120) END AS ChangedFrom, CASE ColName WHEN ''ColA'' THEN ColA_After WHEN ''ColB'' THEN ColB_After WHEN ''ColC'' THEN ColC_After WHEN ''ColD'' THEN ColD_After WHEN ''UDate'' THEN CONVERT(NVARCHAR(50), UDate_After, 120) END AS ChangedTo FROM @MERGERESULTS CROSS APPLY ( VALUES ' + STRING_AGG(N'(''' + COLUMN_NAME + ''')', ',') + N' ) AS CA(ColName) WHERE [Action] = ''UPDATED'' -- 只保留实际有变化的列 AND CASE ColName WHEN ''ColA'' THEN ColA_Before <> ColA_After WHEN ''ColB'' THEN ColB_Before <> ColB_After WHEN ''ColC'' THEN ColC_Before <> ColC_After WHEN ''ColD'' THEN ColD_Before <> ColD_After WHEN ''UDate'' THEN UDate_Before <> UDate_After END = 1 UNION ALL -- 处理Insert的情况 SELECT [Action], Id_After AS Record, NULL AS ChangedFrom, CASE ColName WHEN ''ColA'' THEN ColA_After WHEN ''ColB'' THEN ColB_After WHEN ''ColC'' THEN ColC_After WHEN ''ColD'' THEN ColD_After WHEN ''UDate'' THEN CONVERT(NVARCHAR(50), UDate_After, 120) END AS ChangedTo FROM @MERGERESULTS CROSS APPLY ( VALUES ' + STRING_AGG(N'(''' + COLUMN_NAME + ''')', ',') + N' ) AS CA(ColName) WHERE [Action] = ''INSERTED'' UNION ALL -- 处理Delete的情况 SELECT [Action], Id_Before AS Record, CASE ColName WHEN ''ColA'' THEN ColA_Before WHEN ''ColB'' THEN ColB_Before WHEN ''ColC'' THEN ColC_Before WHEN ''ColD'' THEN ColD_Before WHEN ''UDate'' THEN CONVERT(NVARCHAR(50), UDate_Before, 120) END AS ChangedFrom, NULL AS ChangedTo FROM @MERGERESULTS CROSS APPLY ( VALUES ' + STRING_AGG(N'(''' + COLUMN_NAME + ''')', ',') + N' ) AS CA(ColName) WHERE [Action] = ''DELETED'' ) AS AuditData ORDER BY Record, [Action], ColName; '; -- 执行动态SQL,传入表变量 EXEC sp_executesql @query, N'@MERGERESULTS MERGERESULTS_TYPE READONLY', @MERGERESULTS = @MERGERESULTS; END GO
代码关键细节解释
- MERGE输出捕获:通过
OUTPUT $action, Inserted.*, Deleted.*把所有变更前后的数据存入表变量,包含三种操作类型(INSERTED/UPDATED/DELETED)。 - 动态列适配:通过
INFORMATION_SCHEMA.COLUMNS自动读取目标表的列名,就算表结构变更(增删列),存储过程也不需要修改。 - 列转行处理:用
CROSS APPLY (VALUES (...))把每个列转成单独的行,让每个列的Before/After对应一条审计记录。 - 变更过滤:对于Update操作,只保留
Before <> After的列,避免生成无意义的审计记录。 - 格式统一:分别处理三种操作类型,输出完全符合你要求的审计表结构。
测试验证
用你提供的测试数据执行EXEC MERGE_TEST,会得到如下审计结果(示例):
| Action | Record | ChangedFrom | ChangedTo |
|---|---|---|---|
| UPDATED | 1 | ColA_After | ColA |
| UPDATED | 1 | ColB_After | ColB |
| UPDATED | 1 | ColC_After | ColC |
| UPDATED | 1 | ColD_After | ColD |
| UPDATED | 1 | 2024-05-20 10:00:00 | 2024-05-20 11:00:00 |
如果是Insert操作,会生成每条列的ChangedFrom=NULL记录;Delete操作则生成ChangedTo=NULL的记录,完全匹配你的需求。
内容的提问来源于stack exchange,提问作者Abhilash Gopalakrishna
相关产品推荐
相关产品推荐

