Создание триггеров аудита в SQL Server

Мне нужно реализовать отслеживание изменений в двух таблицах в моей базе данных SQL Server 2005. Мне нужно проверить дополнения, удаления, обновления (подробно о том, что было обновлено). Я планировал использовать триггер, чтобы сделать это, но после того, как я вошел в Google, я обнаружил, что было невероятно легко сделать это неправильно, и я хотел избежать этого на ходу.

Может ли кто-нибудь опубликовать пример триггера обновления, который успешно и элегантно выполнит это? Я надеюсь получить таблицу аудита со следующей структурой:

  • ID
  • LogDate
  • TableName
  • TransactionType (обновление/вставка/удаление)
  • RecordID
  • FieldName
  • OldValue
  • NewValue

... но я открыт для предложений.

Спасибо!

Ответ 1

Я просто хочу вызвать пару точек:

Использовать генераторы кода У вас не может быть единой процедуры для отслеживания всех таблиц, вам нужно будет генерировать похожие, но различные триггеры для каждой отслеживаемой таблицы. Такая работа лучше всего подходит для создания автоматизированного кода. На вашем месте я бы использовал преобразование XSLT для генерации кода из XML, и XML можно было автоматически генерировать из метаданных. Это позволяет легко поддерживать триггеры, регенерируя их каждый раз, когда вы вносите изменения в логику/структуру аудита, или добавляется/изменяется целевая таблица.

Рассмотрите планирование емкости для аудита. Таблица аудита, которая отслеживает все изменения значений, будет, безусловно, самой большой таблицей в базе данных: она будет содержать все текущие данные и всю историю текущих данных. Такая таблица увеличит размер базы данных на 2-3 порядка (x10, x100). И таблица аудита быстро станет узким местом всего:

  • для каждой операции DML потребуются блокировки в таблице аудита.
  • все операции администрирования и обслуживания должны будут учитывать размер базы данных из-за аудита.

Учитывайте изменения схемы. Таблицу с именем "Foo" можно удалить, а позже может быть создана другая таблица с именем "Foo". Аудиторский след должен быть способен различать два разных объекта. Лучше использовать медленно изменяющийся размер.

Учитывайте необходимость эффективно удалять записи аудита. Когда срок хранения, продиктованный политиками, относящимися к вашей заявке, является обязательным, вы должны иметь возможность удалить записи о надлежащей аудиторской проверке. Сейчас это может показаться не таким большим, но спустя 5 лет, когда первые записи будут занесены в таблицу аудита, она выросла до 9,5 ТБ, это может быть проблемой.

Рассмотрим необходимость запросить аудит. Структура таблицы аудита должна быть готова эффективно реагировать на запросы аудита. Если ваш аудит не может быть запрошен, тогда он не имеет значения. Запросы будут полностью зависеть от ваших требований, и только вы знаете их, но большинство записей аудита запрашиваются для временных интервалов ( "какие изменения произошли вчера с 7 вечера до 8 вечера?" ), По объекту ( "какие изменения произошли с этой записью в этом table? ') или автором (" какие изменения сделал Боб в базе данных?").

Ответ 2

Мы используем ApexSQL Audit, который генерирует триггеры аудита и ниже - структуры данных, используемые этим инструментом. Если вы не планируете покупать стороннее решение, вы можете установить этот инструмент в пробном режиме, посмотреть, как они реализовали триггеры и хранилище, а затем создать что-то похожее для себя.

Я не беспокоился о том, чтобы получить слишком много информации о том, как работают эти таблицы, но, надеюсь, это поможет вам начать работу.

enter image description here

Ответ 3

Нет никакого общего способа сделать это так, как вы хотите. В конечном итоге вы в конечном итоге пишете коды кода для каждой таблицы. Не говоря уже о том, что это может быть очень медленно, если вам нужно сравнить каждый столбец для изменения.

Также тот факт, что вы можете обновлять несколько строк одновременно, подразумевает, что вам нужно открыть курсор для прокрутки всех записей.

То, как я это сделаю, будет использовать таблицу со структурой, идентичной таблицам, которые вы отслеживаете, и не открывать ее позже, чтобы показать, какие столбцы действительно изменились. Я также буду следить за сеансом, который на самом деле внес изменения. Это предполагает, что у вас есть первичный ключ в отслеживаемой таблице.

Итак, для таблицы, подобной этой

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

Я бы создал таблицу аудита, подобную этой, в аудиторской шмее.

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 и основного ключа и создаете отчет. В качестве альтернативы вы можете иметь пакетное задание, которое периодически проходит через все таблицы таблицы аудита и готовит пару имя-значение, показывающую изменения.

НТН

Ответ 4

Майк, мы используем инструмент www.auditdatabase.com, этот бесплатный инструмент генерирует триггеры аудита, и он хорошо работает с SQL Server 2008 и 2005 и 2000. Его сложный и безумный инструмент, который позволяет настраивать триггеры аудита для таблицы.

Еще один отличный инструмент - Apex SQL Audit

Ответ 5

Я брошу свой подход и предложения в микс.

У меня очень похожая таблица с предлагаемым дизайном, который я использовал в течение последних семи лет в базе данных SQL 2005 (сейчас 2008).

Я добавил триггеры добавления, обновления и удаления в выбранные таблицы, а затем проверил изменения в выбранных полях. В то время это было просто и хорошо работает.

Вот проблемы, которые я нахожу при таком подходе:

  • Поля для старых/новых значений таблицы аудита должны были быть типами varchar (MAX), чтобы иметь возможность обрабатывать все различные значения, которые могут быть проверены: int, bool, decimal, float, varchar и т.д. для соответствия

  • Код для проверки для каждого поля является утомительным для записи поддержки. Также легко пропустить вещи (например, изменить нулевое поле на значение, которое не попало, потому что значение NULL!= Равно NULL.

  • Удалить запись: как вы это записываете? Все поля? Выбранные? Он усложняется.

Мое будущее видение заключается в использовании некоторого кода SQL-CLR и написании генерируемого триггера, который выполняется, и проверяет метаданные таблицы, чтобы увидеть, что делать. Во-вторых, значения New/Old будут преобразованы в поля XML и весь объект записан: это приводит к большему количеству данных, но удаление имеет целую запись. В Интернете есть несколько статей о триггерах аудита XML.

Ответ 6

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

Ответ 7

Используется триггер Если вы изменяете или вставляете в определенную таблицу, это будет выполняться, и вы можете проверить конкретный столбец в триггере. Полный пример с пояснением приведен на следующем веб-сайте. http://www.allinworld99.blogspot.com/2015/04/triggers-in-sql.html

Ответ 8

Наконец-то я нашел универсальное решение, которое не требует динамических изменений sql и logs для всех столбцов.

Не нужно менять триггер, если таблица изменяется.

Это журнал аудита:

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 имеет первичный ключевой столбец с идентификатором имени и столбцом с именем LastModifiedBy, который содержит имя пользователя, которое произвело последнее изменение.

Триггер состоит из двух частей, сначала он преобразует столбцы вставленных и удаленных таблиц в строки. Это подробно описано здесь: fooobar.com/questions/74649/...

Затем он соединяет строки (одну строку за столбец) вставленных и удаленных таблиц с помощью первичного ключа и имени поля и записывает строку для каждого измененного столбца. Он НЕ регистрирует изменения ID, TimeStamp или LastModifiedByColumn.

Вы можете вставить собственные имена TableName, Columns.

Вы также можете создать следующую хранимую процедуру, а затем вызвать эту хранимую процедуру для генерации ваших триггеров:

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_'[email protected])
    EXEC ('DROP TRIGGER [dbo].tr_audit_'[email protected])


EXEC ('
CREATE TRIGGER [dbo].[tr_audit_'[email protected]+'] ON [ILSe].[dbo].['[email protected]+'] 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 '[email protected]+' FROM deleted WHERE '[email protected]+' = ISNULL(ins.PK,del.PK)) THEN ''I'' WHEN NOT EXISTS (SELECT '[email protected]+' FROM inserted WHERE '[email protected]+' = ISNULL(ins.PK,del.PK)) THEN ''D'' ELSE ''U'' END as [Type],
        '''[email protected]+''' 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.'[email protected]+' as PK,
                  i.'[email protected]+' 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.'[email protected]+' as PK,
                  d.'[email protected]+' 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 ('''[email protected]+''', '''[email protected]+''', '''[email protected]+''') 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

Ответ 9

Каждая таблица, которую нужно отслеживать, будет иметь свой собственный триггер. Совершенно очевидно, что, как указано в принятом ответе, генерация кода будет хорошей вещью.

Если вам нравится этот подход, может возникнуть идея использовать этот триггер и заменить некоторые общие шаги сгенерированным кодом для каждой таблицы отдельно.

Тем не менее я создал полностью общий аудитор-триггер. Наблюдаемая таблица должна иметь PK, но этот PK может быть даже несколькими столбцами.

Некоторые типы столбцов (например, BLOB) могут не работать, но вы можете легко их исключить.

Это не будет лучшим в производительности: -D

Честно говоря: Это скорее упражнение...

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

- Некоторая таблица для проверки этого (использовались причудливые столбцы 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);

- Некоторые тестовые данные

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 [email protected]
         AND [email protected]
       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 [email protected] AND [email protected]
        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

- Теперь протестируйте его с помощью некоторых операций:

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>

Результат selective обновления

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

Ответ 10

Существует общий способ сделать это.

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]