在SQL Server中创build审计触发器

我需要在SQL Server 2005数据库中的两个表上实现更改跟踪。 我需要审计添加,删除,更新(有关更新内容的详细信息)。 我打算使用触发器来做到这一点,但在Google上发现后,我发现这样做很不容易,我想避免这种情况。

任何人都可以发布一个更新触发器的例子,以成功和优雅的方式完成这一点? 我希望结束以下结构的审计表:

  • ID
  • LOGDATE
  • 表名
  • TransactionType(更新/插入/删除)
  • 的recordId
  • 字段名
  • 的OldValue
  • 的NewValue

…但我很乐意提供build议。

谢谢!

我只想说出几点:

使用代码生成器您不能让一个过程来跟踪所有表,您需要在每个跟踪的表上生成类似但不同的触发器。 这种工作最适合自动生成代码。 在你的地方,我将使用XSLT转换来从XML生成代码,并且可以从元数据自动生成XML。 这使您可以在每次更改审计逻辑/结构或者添加/更改目标表时通过重新生成触发器来轻松维护触发器。

考虑审计的容量规划 。 跟踪所有值更改的审计表是迄今为止数据库中最大的表:它将包含当前所有数据和当前数据的所有历史logging。 这样的表将数据库大小增加2-3个数量级(x10,x100)。 审计表将很快成为一切的瓶颈:

  • 每个DML操作都需要在审计表中locking
  • 所有的pipe理和维护操作都将由于审计而适应数据库的大小

考虑架构更改 。 名为“Foo”的表可能会被删除,之后可能会创build一个名为“Foo”的不同表。 审计线索必须能够区分两个不同的对象。 更好地使用缓慢变化的维度方法。

考虑是否需要有效地删除审计logging。 当您的应用程序主题策略规定的保留期限到期时,您需要能够删除到期的审计logging。 现在看起来可能不是什么大不了的事,但5年后,当第一笔logging到期时,审计表已经增长到9.5TB,这可能是一个问题。

考虑需要查询审计 。 审计表结构必须准备好,以有效地回应审计查询。 如果你的审计不能被查询,那么它就没有价值。 查询将完全由您的要求驱动,只有您知道这些查询,但大多数审计logging是按时间间隔查询的(“昨天晚上7点到8点之间发生了什么变化?”),按对象查询(“这个logging发生了什么变化表?')还是作者(“数据库中Bob做了什么修改?”)。

我们正在使用生成审计触发器的ApexSQL审计 ,下面是该工具使用的数据结构。 如果您不打算购买第三方解决scheme,则可以在试用模式下安装此工具,查看他们如何实施触发器和存储,然后为自己创build类似的东西。

我并没有深入了解这些表格的工作细节,但希望这会让你开始。

在这里输入图像描述

没有通用的方式来按照你想要的方式来做。 最终,你最终会为每个表编写大量的代码。 更何况,如果你需要比较每一列的变化,它可以是仙子慢。

另外,您可能会同时更新多行,这意味着您需要打开一个光标来遍历所有logging。

我会这样做的方式是使用与正在跟踪的表相同的结构,稍后将其转换为未显示哪些列实际发生了更改。 我还会跟踪实际进行更改的会话。 这假设你在被跟踪的表中有主键。

所以给了这样一个表

CREATE TABLE TestTable (ID INT NOT NULL CONSTRAINT PK_TEST_TABLE PRIMARY KEY, Name1 NVARCHAR(40) NOT NULL, Name2 NVARCHAR(40)) 

我会在审计schmea中创build一个像这样的审计表。

 CREATE TABLE Audit.TestTable (SessionID UNIQUEIDENTIFER NOT NULL, ID INT NOT NULL, Name1 NVARCHAR(40) NOT NULL, Name2 NVARCHAR(40), Action NVARCHAR(10) NOT NULL CONSTRAINT CK_ACTION CHECK(Action In 'Deleted','Updated'), RowType NVARCHAR(10) NOT NULL CONSTRAINT CK_ROWTYPE CHECK (RowType in 'New','Old','Deleted'), ChangedDate DATETIME NOT NULL Default GETDATE(), ChangedBy SYSNHAME NOT NULL DEFAULT USER_NAME()) 

并像这样的更新触发器

 CREATE Trigger UpdateTestTable ON DBO.TestTable FOR UPDATE AS BEGIN SET NOCOUNT ON DECLARE @SessionID UNIQUEIDENTIFER SET @SessionID = NEWID() INSERT Audit.TestTable(Id,Name1,Name2,Action,RowType,SessionID) SELECT ID,name1,Name2,'Updated','Old',@SessionID FROM Deleted INSERT Audit.TestTable(Id,Name1,Name2,Action,RowType,SessionID) SELECT ID,name1,Name2,'Updated','New',@SessionID FROM Inserted END 

这运行得非常快。 在报告期间,您只需根据sessionID和主键联接行并生成报告。 或者,您可以有一个批处理作业定期遍历审核表中的所有表,并准备一个名称 – 值对来显示更改。

HTH

迈克,我们正在使用www.auditdatabase.com工具,这个免费的工具生成审计触发器,它适用于SQL Server 2008和2005年和2000年。其复杂和疯狂的工具,允许自定义审计触发器的表。

另一个出色的工具是Apex SQL Audit

我会投入我的方法和build议的混合。

我有一个非常相似的表格,这个表格与我在SQL 2005(现在的2008)数据库上使用了七年的build议devise很相似。

我添加插入,更新和删除触发选定的表,然后检查选定的字段的变化。 当时它很简单,运作良好。

以下是我用这种方法find的问题:

  1. 审计表的旧/新值字段必须是varchar(MAX)types,以便能够处理所有可以被审计的不同值:int,bool,decimal,float,varchar等,都必须符合

  2. 检查每个字段的代码是非常繁琐的写一个维护。 这也很容易错过(比如将一个空字段更改为一个没有被捕获的值,因为NULL!= value是NULL。

  3. 删除logging:你如何logging这个? 所有领域? select的? 它变得复杂

我的未来愿景是使用一些SQL-CLR代码并编写一个执行的通用触发器,并检查表元数据以查看要审计的内容。 其次,新值/旧值将被转换为XML字段并logging整个对象:这导致更多的数据,但是删除具有整个logging。 Web上有几篇关于XML审计触发器的文章。

 CREATE TRIGGER TriggerName ON TableName FOR INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON DECLARE @ExecStr varchar(50), @Qry nvarchar(255) CREATE TABLE #inputbuffer ( EventType nvarchar(30), Parameters int, EventInfo nvarchar(255) ) SET @ExecStr = 'DBCC INPUTBUFFER(' + STR(@@SPID) + ')' INSERT INTO #inputbuffer EXEC (@ExecStr) SET @Qry = (SELECT EventInfo FROM #inputbuffer) SELECT @Qry AS 'Query that fired the trigger', SYSTEM_USER as LoginName, USER AS UserName, CURRENT_TIMESTAMP AS CurrentTime END 

触发器用于如果您修改或插入特定的表,这将执行,您可以检查触发器中的特定列。 完整的例子和解释在下面的网站。 http://www.allinworld99.blogspot.com/2015/04/triggers-in-sql.html

我终于find了一个通用的解决scheme,即不需要dynamic的sql和日志更改所有列。

如果表更改,则不需要更改触发器。

这是审计日志:

 CREATE TABLE [dbo].[Audit]( [ID] [bigint] IDENTITY(1,1) NOT NULL, [Type] [char](1) COLLATE Latin1_General_CI_AS NULL, [TableName] [nvarchar](128) COLLATE Latin1_General_CI_AS NULL, [PK] [int] NULL, [FieldName] [nvarchar](128) COLLATE Latin1_General_CI_AS NULL, [OldValue] [nvarchar](max) COLLATE Latin1_General_CI_AS NULL, [NewValue] [nvarchar](max) COLLATE Latin1_General_CI_AS NULL, [UpdateDate] [datetime] NULL, [Username] [nvarchar](8) COLLATE Latin1_General_CI_AS NULL, CONSTRAINT [PK_AuditB] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 

这是一个表的触发器:

 INSERT INTO ILSe.dbo.Audit ([Type], TableName, PK, FieldName, OldValue, NewValue, Username) SELECT CASE WHEN NOT EXISTS (SELECT ID FROM deleted WHERE ID = ISNULL(ins.PK,del.PK)) THEN 'I' WHEN NOT EXISTS (SELECT ID FROM inserted WHERE ID = ISNULL(ins.PK,del.PK)) THEN 'D' ELSE 'U' END as [Type], 'AGB' as TableName, ISNULL(ins.PK,del.PK) as PK, ISNULL(ins.FieldName,del.FieldName) as FieldName, del.FieldValue as OldValue, ins.FieldValue as NewValue, ISNULL(ins.Username,del.Username) as Username FROM (SELECT insRowTbl.PK, insRowTbl.Username, attr.insRow.value('local-name(.)', 'nvarchar(128)') as FieldName, attr.insRow.value('.', 'nvarchar(max)') as FieldValue FROM (Select i.ID as PK, i.LastModifiedBy as Username, convert(xml, (select i.* for xml raw)) as insRowCol from inserted as i ) as insRowTbl CROSS APPLY insRowTbl.insRowCol.nodes('/row/@*') as attr(insRow) ) as ins FULL OUTER JOIN (SELECT delRowTbl.PK, delRowTbl.Username, attr.delRow.value('local-name(.)', 'nvarchar(128)') as FieldName, attr.delRow.value('.', 'nvarchar(max)') as FieldValue FROM (Select d.ID as PK, d.LastModifiedBy as Username, convert(xml, (select d.* for xml raw)) as delRowCol from deleted as d ) as delRowTbl CROSS APPLY delRowTbl.delRowCol.nodes('/row/@*') as attr(delRow) ) as del on ins.PK = del.PK and ins.FieldName = del.FieldName WHERE isnull(ins.FieldName,del.FieldName) not in ('LastModifiedBy', 'ID', 'TimeStamp') and ((ins.FieldValue is null and del.FieldValue is not null) or (ins.FieldValue is not null and del.FieldValue is null) or (ins.FieldValue != del.FieldValue)) 

这个触发器是一个名为AGB的表。 名称为AGB的表格具有名称为ID的主键列和名称为LastModifiedBy的列,其中包含最后编辑的用户名。

触发器由两部分组成,首先将插入和删除表的列转换为行。 这在这里详细解释: https : //stackoverflow.com/a/43799776/4160788

然后通过主键和字段名称连接插入和删除表的行(每列一行),并为每个更改的列logging一行。 它不会loggingID,TimeStamp或LastModifiedByColumn的更改。

您可以插入自己的TableName,Columns名称。

你也可以创build下面的存储过程,然后调用这个存储过程来生成你的触发器:

 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[_create_audit_trigger]') AND type in (N'P', N'PC')) BEGIN EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[_create_audit_trigger] AS' END ALTER PROCEDURE [dbo].[_create_audit_trigger] @TableName varchar(max), @IDColumnName varchar(max) = 'ID', @LastModifiedByColumnName varchar(max) = 'LastModifiedBy', @TimeStampColumnName varchar(max) = 'TimeStamp' AS BEGIN PRINT 'start ' + @TableName + ' (' + @IDColumnName + ', ' + @LastModifiedByColumnName + ', ' + @TimeStampColumnName + ')' /* if you have other audit trigger on this table and want to disable all triggers, enable this: EXEC ('ALTER TABLE ' + @TableName + ' DISABLE TRIGGER ALL')*/ IF EXISTS (SELECT * FROM sys.objects WHERE [type] = 'TR' AND [name] = 'tr_audit_'+@TableName) EXEC ('DROP TRIGGER [dbo].tr_audit_'+@TableName) EXEC (' CREATE TRIGGER [dbo].[tr_audit_'+@TableName+'] ON [ILSe].[dbo].['+@TableName+'] FOR INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON; INSERT INTO ILSe.dbo.Audit ([Type], TableName, PK, FieldName, OldValue, NewValue, Username) SELECT CASE WHEN NOT EXISTS (SELECT '+@IDColumnName+' FROM deleted WHERE '+@IDColumnName+' = ISNULL(ins.PK,del.PK)) THEN ''I'' WHEN NOT EXISTS (SELECT '+@IDColumnName+' FROM inserted WHERE '+@IDColumnName+' = ISNULL(ins.PK,del.PK)) THEN ''D'' ELSE ''U'' END as [Type], '''+@TableName+''' as TableName, ISNULL(ins.PK,del.PK) as PK, ISNULL(ins.FieldName,del.FieldName) as FieldName, del.FieldValue as OldValue, ins.FieldValue as NewValue, ISNULL(ins.Username,del.Username) as Username FROM (SELECT insRowTbl.PK, insRowTbl.Username, attr.insRow.value(''local-name(.)'', ''nvarchar(128)'') as FieldName, attr.insRow.value(''.'', ''nvarchar(max)'') as FieldValue FROM (Select i.'+@IDColumnName+' as PK, i.'+@LastModifiedByColumnName+' as Username, convert(xml, (select i.* for xml raw)) as insRowCol from inserted as i) as insRowTbl CROSS APPLY insRowTbl.insRowCol.nodes(''/row/@*'') as attr(insRow)) as ins FULL OUTER JOIN (SELECT delRowTbl.PK, delRowTbl.Username, attr.delRow.value(''local-name(.)'', ''nvarchar(128)'') as FieldName, attr.delRow.value(''.'', ''nvarchar(max)'') as FieldValue FROM (Select d.'+@IDColumnName+' as PK, d.'+@LastModifiedByColumnName+' as Username, convert(xml, (select d.* for xml raw)) as delRowCol from deleted as d) as delRowTbl CROSS APPLY delRowTbl.delRowCol.nodes(''/row/@*'') as attr(delRow)) as del on ins.PK = del.PK and ins.FieldName = del.FieldName WHERE isnull(ins.FieldName,del.FieldName) not in ('''+@LastModifiedByColumnName+''', '''+@IDColumnName+''', '''+@TimeStampColumnName+''') and ((ins.FieldValue is null and del.FieldValue is not null) or (ins.FieldValue is not null and del.FieldValue is null) or (ins.FieldValue != del.FieldValue)) END ') PRINT 'end ' + @TableName PRINT '' END 

每个想要监视的表都需要自己的触发器。 这是非常明显的,正如在接受的答案中指出的那样,代码生成将是一件好事。

如果你喜欢这种方法,那么使用这个触发器可能是一个想法,并用每个表的生成代码分别replace一些通用步骤。

不过,我创build了一个完全通用的审计触发器 。 观察表必须有一个PK ,但这个PK甚至可能是多列

某些列types(如BLOB)可能无法正常工作,但您可以轻松排除它们。

这不会是performance最好的:-D

说实话:这是更多的练习…

 SET NOCOUNT ON; GO CREATE TABLE AuditTest(ID UNIQUEIDENTIFIER ,LogDate DATETIME ,TableSchema VARCHAR(250) ,TableName VARCHAR(250) ,AuditType VARCHAR(250),Content XML); GO 

– 一些表来testing这个(使用古怪的PK专栏…)

 CREATE TABLE dbo.Testx(ID1 DATETIME NOT NULL ,ID2 UNIQUEIDENTIFIER NOT NULL ,Test1 VARCHAR(100) ,Test2 DATETIME); --Add a two column PK ALTER TABLE dbo.Testx ADD CONSTRAINT PK_Test PRIMARY KEY(ID1,ID2); 

– 一些testing数据

 INSERT INTO dbo.Testx(ID1,ID2,Test1,Test2) VALUES ({d'2000-01-01'},NEWID(),'Test1',NULL) ,({d'2000-02-01'},NEWID(),'Test2',{d'2002-02-02'}); 

– 这是目前的内容

 SELECT * FROM dbo.Testx; GO 

– 审计的触发器

  CREATE TRIGGER [dbo].[UpdateTestTrigger] ON [dbo].[Testx] FOR UPDATE,INSERT,DELETE AS BEGIN IF NOT EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) RETURN; SET NOCOUNT ON; DECLARE @tableSchema VARCHAR(250); DECLARE @tableName VARCHAR(250); DECLARE @AuditID UNIQUEIDENTIFIER=NEWID(); DECLARE @LogDate DATETIME=GETDATE(); SELECT @tableSchema = sch.name ,@tableName = tb.name FROM sys.triggers AS tr INNER JOIN sys.tables AS tb ON tr.parent_id=tb.object_id INNER JOIN sys.schemas AS sch ON tb.schema_id=sch.schema_id WHERE tr.object_id = @@PROCID DECLARE @tp VARCHAR(10)=CASE WHEN EXISTS(SELECT 1 FROM deleted) AND EXISTS(SELECT 1 FROM inserted) THEN 'upd' ELSE CASE WHEN EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) THEN 'del' ELSE 'ins' END END; SELECT * INTO #tmpInserted FROM inserted; SELECT * INTO #tmpDeleted FROM deleted; SELECT kc.ORDINAL_POSITION, kc.COLUMN_NAME INTO #tmpPKColumns FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kc ON tc.TABLE_CATALOG=kc.TABLE_CATALOG AND tc.TABLE_SCHEMA=kc.TABLE_SCHEMA AND tc.TABLE_NAME=kc.TABLE_NAME AND tc.CONSTRAINT_NAME=kc.CONSTRAINT_NAME AND tc.CONSTRAINT_TYPE='PRIMARY KEY' WHERE tc.TABLE_SCHEMA=@tableSchema AND tc.TABLE_NAME=@tableName ORDER BY kc.ORDINAL_POSITION; DECLARE @pkCols VARCHAR(MAX)= STUFF ( ( SELECT 'UNION ALL SELECT ''' + pc.COLUMN_NAME + ''' AS [@name] , CAST(COALESCE(i.' + QUOTENAME(pc.COLUMN_NAME) + ',d.' + QUOTENAME(pc.COLUMN_NAME) + ') AS VARCHAR(MAX)) AS [@value] ' FROM #tmpPKColumns AS pc ORDER BY pc.ORDINAL_POSITION FOR XML PATH('') ),1,16,''); DECLARE @pkColsCompare VARCHAR(MAX)= STUFF ( ( SELECT 'AND i.' + QUOTENAME(pc.COLUMN_NAME) + '=d.' + QUOTENAME(pc.COLUMN_NAME) FROM #tmpPKColumns AS pc ORDER BY pc.ORDINAL_POSITION FOR XML PATH('') ),1,3,''); DECLARE @cols VARCHAR(MAX)= STUFF ( ( SELECT ',' + CASE WHEN @tp='upd' THEN 'CASE WHEN (i.[' + COLUMN_NAME + ']!=d.[' + COLUMN_NAME + '] ' + 'OR (i.[' + COLUMN_NAME + '] IS NULL AND d.[' + COLUMN_NAME + '] IS NOT NULL) ' + 'OR (i.['+ COLUMN_NAME + '] IS NOT NULL AND d.[' + COLUMN_NAME + '] IS NULL)) ' + 'THEN ' ELSE '' END + '(SELECT ''' + COLUMN_NAME + ''' AS [@name]' + CASE WHEN @tp IN ('upd','del') THEN ',ISNULL(CAST(d.[' + COLUMN_NAME + '] AS NVARCHAR(MAX)),N''##NULL##'') AS [@old]' ELSE '' END + CASE WHEN @tp IN ('ins','upd') THEN ',ISNULL(CAST(i.[' + COLUMN_NAME + '] AS NVARCHAR(MAX)),N''##NULL##'') AS [@new] ' ELSE '' END + ' FOR XML PATH(''Column''),TYPE) ' + CASE WHEN @tp='upd' THEN 'END' ELSE '' END FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@tableSchema AND TABLE_NAME=@tableName FOR XML PATH('') ),1,1,'' ); DECLARE @cmd VARCHAR(MAX)= 'SET LANGUAGE ENGLISH; WITH ChangedColumns AS ( SELECT A.PK' + ',A.PK.query(''data(/PK/Column/@value)'').value(''text()[1]'',''nvarchar(max)'') AS PKVals' + ',Col.* FROM #tmpInserted AS i FULL OUTER JOIN #tmpDeleted AS d ON ' + @pkColsCompare + ' CROSS APPLY ( SELECT ' + @cols + ' FOR XML PATH(''''),TYPE ) AS Col([Column]) CROSS APPLY(SELECT (SELECT tbl.* FROM (SELECT ' + @pkCols + ') AS tbl FOR XML PATH(''Column''), ROOT(''PK''),TYPE)) AS A(PK) ) INSERT INTO AuditTest(ID,LogDate,TableSchema,TableName,AuditType,Content) SELECT ''' + CAST(@AuditID AS VARCHAR(MAX)) + ''',''' + CONVERT(VARCHAR(MAX),@LogDate,126) + ''',''' + @tableSchema + ''',''' + @tableName + ''',''' + @tp + ''' ,( SELECT ''' + @tableSchema + ''' AS [@TableSchema] ,''' + @tableName + ''' AS [@TableName] ,''' + @tp + ''' AS [@ActionType] ,( SELECT ChangedColumns.PK AS [*] ,( SELECT x.[Column] AS [*],'''' FROM ChangedColumns AS x WHERE x.PKVals=ChangedColumns.PKVals FOR XML PATH(''Values''),TYPE ) FROM ChangedColumns FOR XML PATH(''Row''),TYPE ) FOR XML PATH(''Changes'') );'; EXEC (@cmd); DROP TABLE #tmpInserted; DROP TABLE #tmpDeleted; END GO 

– 现在让我们用一些操作来testing它:

 UPDATE dbo.Testx SET Test1='New 1' WHERE ID1={d'2000-01-01'}; UPDATE dbo.Testx SET Test1='New 1',Test2={d'2000-01-01'} ; DELETE FROM dbo.Testx WHERE ID1={d'2000-02-01'}; DELETE FROM dbo.Testx WHERE ID1=GETDATE(); --no affect INSERT INTO dbo.Testx(ID1,ID2,Test1,Test2) VALUES ({d'2000-03-01'},NEWID(),'Test3',{d'2001-03-03'}) ,({d'2000-04-01'},NEWID(),'Test4',{d'2001-04-04'}) ,({d'2000-05-01'},NEWID(),'Test5',{d'2001-05-05'}); UPDATE dbo.Testx SET Test2=NULL; --all rows DELETE FROM dbo.Testx WHERE ID1 IN ({d'2000-02-01'},{d'2000-03-01'}); GO 

– 检查最终状态

 SELECT * FROM dbo.Testx; SELECT * FROM AuditTest; GO 

– 清理(注意真实的数据!

 DROP TABLE dbo.Testx; GO DROP TABLE dbo.AuditTest; GO 

插入的结果

 <Changes TableSchema="dbo" TableName="Testx" ActionType="ins"> <Row> <PK> <Column name="ID1" value="May 1 2000 12:00AM" /> <Column name="ID2" value="C2EB4D11-63F8-434E-8470-FB4A422A4ED1" /> </PK> <Values> <Column name="ID1" new="May 1 2000 12:00AM" /> <Column name="ID2" new="C2EB4D11-63F8-434E-8470-FB4A422A4ED1" /> <Column name="Test1" new="Test5" /> <Column name="Test2" new="May 5 2001 12:00AM" /> </Values> </Row> <Row> <PK> <Column name="ID1" value="Apr 1 2000 12:00AM" /> <Column name="ID2" value="28625CE7-9424-4FA6-AEDA-1E4853451655" /> </PK> <Values> <Column name="ID1" new="Apr 1 2000 12:00AM" /> <Column name="ID2" new="28625CE7-9424-4FA6-AEDA-1E4853451655" /> <Column name="Test1" new="Test4" /> <Column name="Test2" new="Apr 4 2001 12:00AM" /> </Values> </Row> <Row> <PK> <Column name="ID1" value="Mar 1 2000 12:00AM" /> <Column name="ID2" value="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" /> </PK> <Values> <Column name="ID1" new="Mar 1 2000 12:00AM" /> <Column name="ID2" new="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" /> <Column name="Test1" new="Test3" /> <Column name="Test2" new="Mar 3 2001 12:00AM" /> </Values> </Row> </Changes> 

更新的select结果

 <Changes TableSchema="dbo" TableName="Testx" ActionType="upd"> <Row> <PK> <Column name="ID1" value="Feb 1 2000 12:00AM" /> <Column name="ID2" value="D7AB263A-EEFC-47DB-A6BB-A559FE8F2119" /> </PK> <Values> <Column name="Test1" old="Test2" new="New 1" /> <Column name="Test2" old="Feb 2 2002 12:00AM" new="Jan 1 2000 12:00AM" /> </Values> </Row> <Row> <PK> <Column name="ID1" value="Jan 1 2000 12:00AM" /> <Column name="ID2" value="318C0A66-8833-4F03-BCEF-7AB78C91704F" /> </PK> <Values> <Column name="Test2" old="##NULL##" new="Jan 1 2000 12:00AM" /> </Values> </Row> </Changes> 

并删除的结果

 <Changes TableSchema="dbo" TableName="Testx" ActionType="del"> <Row> <PK> <Column name="ID1" value="Mar 1 2000 12:00AM" /> <Column name="ID2" value="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" /> </PK> <Values> <Column name="ID1" old="Mar 1 2000 12:00AM" /> <Column name="ID2" old="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" /> <Column name="Test1" old="Test3" /> <Column name="Test2" old="##NULL##" /> </Values> </Row> </Changes> 

有一个通用的方法来做到这一点。

 CREATE TABLE [dbo].[Audit]( [TYPE] [CHAR](1) NULL, [TableName] [VARCHAR](128) NULL, [PK] [VARCHAR](1000) NULL, [FieldName] [VARCHAR](128) NULL, [OldValue] [VARCHAR](1000) NULL, [NewValue] [VARCHAR](1000) NULL, [UpdateDate] [datetime] NULL, [UserName] [VARCHAR](128) NULL ) ON [PRIMARY]