Criar gatilhos de auditoria no servidor SQL

Preciso de implementar o acompanhamento de alterações em duas tabelas na minha base de dados SQL Server 2005. Preciso auditar adições, supressões, atualizações (com detalhes sobre o que foi atualizado). Eu estava planejando usar um gatilho para fazer isso, mas depois de bisbilhotar no Google eu descobri que era incrivelmente fácil fazer isso incorretamente, e eu queria evitar isso no get-go.

Alguém pode postar um exemplo de uma atualização desencadeia isso com sucesso e de uma maneira elegante? Espero que sim. acabar com um quadro de auditoria com a seguinte estrutura:

  • ID
  • Data de Registo
  • Nome de Tabela
  • TransactionType (update/insert/delete)
  • Record
  • Nome de Campo
  • OldValue
  • NewValue

... mas estou aberto a sugestões.

Obrigado!

Author: Mike Cole, 2009-12-26

10 answers

Só quero chamar a atenção para alguns pontos:

Use code generators Você não pode ter um único procedimento para rastrear todas as tabelas, você terá que gerar gatilhos semelhantes mas distintos em cada tabela rastreada. Este tipo de trabalho é mais adequado para a geração de código automatizado. Em seu lugar eu usaria uma transformação XSLT para gerar o código a partir de XML, e o XML pode ser gerado automaticamente a partir de metadados. Isto permite-lhe manter facilmente os gatilhos regenerando-os cada vez que você faz uma alteração na lógica/estrutura de auditoria ou uma tabela alvo é adicionada/alterada.

Consideraro planeamento da capacidade para a auditoria. Uma tabela de auditoria que acompanha todas as mudanças de valor será, de longe, a maior tabela da base de dados: conterá todos os dados atuais e toda a história dos dados actuais. Tal tabela irá aumentar o tamanho do banco de dados em 2-3 ordens de magnitude (x10, x100). E a tabela de auditoria tornar-se-á rapidamente o gargalo de tudo:

  • cada operação DML exigirá bloqueios na tabela de auditoria
  • Todas as operações administrativas e de manutenção terão de ter em conta o tamanho da base de dados devido à auditoria

Tenha em conta as alterações do esquema . Uma tabela chamada 'Foo' pode ser abandonada e mais tarde uma tabela diferente chamada 'Foo' pode ser criada. A pista de auditoria deve ser capaz de distinguir os dois objetos diferentes. Melhor usar uma dimensão em mudança lenta abordagem.

Considere a necessidade de eliminar eficientemente os registos de auditoria. Quando o período de retenção ditado por suas políticas de assunto de aplicação é devido, você precisa ser capaz de excluir os devidos registros de auditoria. Pode não parecer um grande negócio agora, mas 5 anos mais tarde, quando os primeiros registros são devidos a tabela de auditoria cresceu para 9,5 TB pode ser um problema.

Considere a necessidade de questionar a auditoria . A estrutura do quadro de auditoria tem de estar preparada para responder de forma eficiente ao perguntas sobre auditoria. Se a sua auditoria não pode ser questionada, então ela não tem valor. As perguntas serão inteiramente motivadas pelos seus requisitos e só você sabe disso, mas a maioria dos registros de auditoria são questionados por intervalos de tempo ('que mudanças ocorreram entre as 19h e as 20h de ontem?'), por objeto ('que mudanças ocorreram neste registro nesta tabela?') ou por autor ('que alterações Bob fez na base de dados?').

 35
Author: Remus Rusanu, 2009-12-26 17:19:09

Estamos a utilizar a auditoria ApexSQL que gera gatilhos de auditoria e Abaixo estão as estruturas de dados utilizadas por esta ferramenta. Se você não planeja comprar uma solução de terceiros, você pode instalar esta ferramenta no modo de teste, Veja como eles implementaram gatilhos e armazenamento e, em seguida, criar algo semelhante para si mesmo.

Não me dei ao trabalho de entrar em muitos detalhes sobre como estas mesas funcionam, mas espero que isto te faça começar.

enter image description here

 19
Author: Igor Voplov, 2013-03-29 13:58:53

Não há uma maneira genérica de o fazer da maneira que você quer. Em última análise, você acaba escrevendo resmas de código para cada tabela. Sem mencionar que pode ser fairy slow se você precisar comparar cada coluna para a mudança.

Também o facto de poder estar a actualizar várias linhas ao mesmo tempo implica que precisa de abrir um cursor para percorrer todos os registos.

A forma como eu o faria será usando uma tabela com estrutura idêntica às tabelas que você está rastreando e univotá-lo mais tarde para mostrar qual as colunas mudaram mesmo. Eu também manteria o controle da sessão que realmente fez a mudança. Isto assume que você tem chave primária na tabela sendo rastreada.

Então, dada uma tabela como esta

CREATE TABLE TestTable  
(ID INT NOT NULL CONSTRAINT PK_TEST_TABLE PRIMARY KEY,
Name1 NVARCHAR(40) NOT NULL,  
Name2 NVARCHAR(40))
Eu criaria uma tabela de auditoria como esta na auditoria schmea.
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())

E um gatilho para actualização como esta

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
Isto é muito rápido. Durante a apresentação de relatórios, você simplesmente juntar as linhas com base no sessionID, e chave primária e produzir um relatório. Em alternativa, pode ter um trabalho em lote que passa periodicamente por todas as tabelas da tabela de auditoria e prepara um par nome-valor que mostra as mudanças.

HTH

 13
Author: no_one, 2009-12-26 15:26:35
Mike, estamos a usar www.auditdatabase.com ferramenta, esta ferramenta gratuita gera gatilhos de auditoria e funciona bem com o SQL Server 2008 e 2005 e 2000. É uma ferramenta sofisticada e madura que permite gatilhos de auditoria personalizados para uma tabela.

Outra excelente ferramenta é a auditoria SQL Apex

 1
Author: Camejo, 2010-12-05 14:24:21
Vou juntar a minha abordagem e sugestões para a mistura.

Eu tenho uma tabela muito semelhante ao seu projeto proposto que eu usei nos últimos sete anos em uma base de dados SQL 2005 (agora 2008).

Adicionei inserir, actualizar e apagar os gatilhos às tabelas seleccionadas, e depois verifiquei as alterações nos campos seleccionados. Na época era simples e funciona bem.

Eis as questões que encontro com esta abordagem:
  1. A tabela de auditoria antigos / novos campos de valor teve de ser tipos varchar (MAX) para ser capaz de lidar com todos os diferentes valores que poderiam ser auditados: int,bool,decimal,float,varchar, etc. todos têm de caber.

  2. O código a verificar para cada campo é tedioso para escrever uma manutenção. Também é fácil perder as coisas (como mudar um campo nulo para um valor não foi pego, porque nulo != valor é nulo.

  3. Apagar o registo: como grava isto? Todos os campos? Seleccionados? Fica complicado.

A minha visão futura é ... para usar algum código SQL-CLR e escrever um gatilho genérico que é executado e verifica meta-dados da tabela para ver o que auditar. Em segundo lugar, os valores novos/antigos serão convertidos para campos XML e todo o objeto gravado: isto resulta em mais dados, mas uma delete tem um registro inteiro. Existem vários artigos na web sobre gatilhos de auditoria XML.
 0
Author: Quango, 2013-05-30 17:27:48
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
 0
Author: Rak, 2014-03-06 19:10:23

Trigger é usado se você modificar ou inserir numa tabela em particular que esta irá executar, e você pode verificar a coluna em particular na trigger. Exemplo completo com explicação está no seguinte site. http://www.allinworld99.blogspot.com/2015/04/triggers-in-sql.html

 0
Author: Merbin Joe, 2015-04-13 10:55:27

Finalmente encontrei uma solução universal, que não requer mudanças dinâmicas de SQL e logs de todas as colunas.

Não é necessário alterar o gatilho se a tabela mudar.

Este é o registo de auditoria:

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]

Este é o gatilho para uma tabela:

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))
Este gatilho é para uma tabela chamada AGB. A tabela com o nome AGB tem uma coluna de chave primária com o nome ID e uma coluna com o nome LastModifiedBy, que contém o nome de utilizador que fez a última editar.

A mola é constituída por duas partes, convertendo primeiro colunas de tabelas inseridas e suprimidas em linhas. Isto é explicado em detalhes aqui: https://stackoverflow.com/a/43799776/4160788

Depois junta as linhas (uma linha por coluna) das tabelas inseridas e apagadas por chave primária e nome do campo, e regista uma linha para cada coluna alterada. Não regista alterações de ID, TimeStamp ou LastModifiedByColumn.

Pode inserir o seu próprio nome de mesa, colunas nome.

Você também pode criar o seguinte procedimento armazenado, e então chamar este procedimento armazenado para gerar os seus gatilhos:

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
 0
Author: flack, 2017-05-23 12:10:36
Cada mesa que alguém quiser monitorizar, vai precisar do seu próprio gatilho. É bastante óbvio que - tal como referido na resposta aceite-a geração de código será uma coisa boa.

Se gostou desta abordagem, pode ser uma ideia usar esta activação e substituir alguns passos genéricos com o código gerado para cada tabela separadamente.

No entanto, criei uma auditoria totalmente genérica-Trigger . A tabela observada deve ter um PK , mas este PK pode até ser Multi-coluna .

Alguns tipos de colunas (como bolhas) podem não funcionar, mas você pode facilmente excluí-los.

Este não será o melhor no desempenho : - D

Para ser honesto, isto é mais um tipo de exercício...

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

-- alguma tabela para testar isto (usou colunas de PK excêntricas de propósito...)

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);

Alguns dados de ensaio

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'});

--Este é o conteúdo actual

SELECT * FROM dbo.Testx;
GO

-- o gatilho para o auditoria

    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
Agora vamos testá - lo com algumas operações.
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

--verifique o estado final

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

--Clean up (carefull com dados reais!)

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

O resultado da inserção

<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>

O resultado selectivo de uma actualização

<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>

E o resultado de uma eliminação

<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>
 0
Author: Shnugo, 2017-08-18 23:36:16
Há uma forma genérica de o fazer.
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] 
 -10
Author: carlito, 2013-05-17 16:22:41