2009-08-10 4 views
12

Мне известно о COLUMNS_UPDATED, ну мне нужен быстрый ярлык (если кто-то сделал, я уже делаю его, но если кто-то может сэкономить мое время, я его буду использовать)Триггер обновления SQL Server, получить только измененные поля

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

SELECT * FROM insert дает мне каждый столбец, но мне нужны только обновленные.

что-то вроде следующих ...

CREATE TRIGGER DBCustomers_Insert 
    ON DBCustomers 
    AFTER UPDATE 
AS 
BEGIN 
    DECLARE @sql as NVARCHAR(1024); 
    SET @sql = 'SELECT '; 


    I NEED HELP FOR FOLLOWING LINE ...., I can manually write every column, but I need 
    an automated routin which can work regardless of column specification 
    for each column, if its modified append $sql = ',' + columnname... 

    SET @sql = $sql + ' FROM inserted FOR XML RAW'; 

    DECLARE @x as XML; 
    SET @x = CAST(EXEC(@sql) AS XML); 


    .. use @x 

END 

ответ

13

Внутри триггера, вы можете использовать COLUMNS_UPDATED() как это для того, чтобы получить обновленное значение

-- Get the table id of the trigger 
-- 
DECLARE @idTable  INT 

SELECT @idTable = T.id 
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id 
WHERE P.id = @@procid 

-- Get COLUMNS_UPDATED if update 
-- 
DECLARE @Columns_Updated VARCHAR(50) 

SELECT @Columns_Updated = ISNULL(@Columns_Updated + ', ', '') + name 
FROM syscolumns 
WHERE id = @idTable 
AND  CONVERT(VARBINARY,REVERSE(COLUMNS_UPDATED())) & POWER(CONVERT(BIGINT, 2), colorder - 1) > 0 

Но это snipet кода терпит неудачу, когда у вас есть таблица с более чем 62 колонок .. Арт. Переполнение ...

Вот окончательная версия, которая обрабатывает более 62 столбцов, но дает только количество обновленных столбцов. Это легко связать с «SYSCOLUMNS», чтобы получить имя

DECLARE @Columns_Updated VARCHAR(100) 
SET  @Columns_Updated = '' 

DECLARE @maxByteCU INT 
DECLARE @curByteCU INT 
SELECT @maxByteCU = DATALENGTH(COLUMNS_UPDATED()), 
     @curByteCU = 1 

WHILE @curByteCU <= @maxByteCU BEGIN 
    DECLARE @cByte INT 
    SET  @cByte = SUBSTRING(COLUMNS_UPDATED(), @curByteCU, 1) 

    DECLARE @curBit INT 
    DECLARE @maxBit INT 
    SELECT @curBit = 1, 
      @maxBit = 8 
    WHILE @curBit <= @maxBit BEGIN 
     IF CONVERT(BIT, @cByte & POWER(2,@curBit - 1)) <> 0 
      SET @Columns_Updated = @Columns_Updated + '[' + CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) + ']' 
     SET @curBit = @curBit + 1 
    END 
    SET @curByteCU = @curByteCU + 1 
END 
+0

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

+0

Приятно, но для информации, когда в столбце sys.columns (из упавшего столбца) есть пробел в столбце_ид, этот код не обнаруживает. Ex с таблицей из 26 столбцов, результат [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27], в то время как нет столбца с column_id = 9 – Yahia

+0

не работает должным образом. Иногда он работает для 1 поля, иногда он работает, иногда он полностью завершается (он основан на запросе, но не рандомизированном выходе для одного и того же запроса). Я использую SQL Server 2014 (120)! – SKLTFZ

1

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

Declare @sql nvarchar(4000) 
DECLARE @ParmDefinition nvarchar(500) 
Declare @OutString varchar(8000) 
Declare @tbl sysname 

Set @OutString = '' 
Set @tbl = 'SomeTable' --The table we are interested in 
--Store the contents of deleted in temp table 
Select * into #tempDelete from deleted 
--Build sql string based on definition 
--of table 
--to retrieve the column name 
--or empty string 
--based on comparison between 
--target table and temp table 
set @sql = '' 
Select @sql = @sql + 'Case when IsNull(i.[' + Column_Name + 
'],0) = IsNull(d.[' + Column_name + '],0) then '''' 
else ' + quotename(Column_Name, char(39)) + ' + '',''' + ' end +' 
from information_schema.columns 
where table_name = @tbl 
--Define output parameter 
set @ParmDefinition = '@OutString varchar(8000) OUTPUT' 
--Format sql 
set @sql = 'Select @OutString = ' 
+ Substring(@sql,1 , len(@sql) -1) + 
' From SomeTable i ' --Will need to be updated for target schema 
+ ' inner join #tempDelete d on 
i.PK = d.PK ' --Will need to be updated for target schema 
--Execute sql and retrieve desired column list in output parameter 
exec sp_executesql @sql, @ParmDefinition, @OutString OUT 
drop table #tempDelete 
--strip trailing column if a non-zero length string 
--was returned 
if Len(@Outstring) > 0 
    Set @OutString = Substring(@OutString, 1, Len(@Outstring) -1) 
--return comma delimited list of changed columns. 
Select @OutString 
End 
14

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

В основном вы начинаете с the inserted and deleted tables, не открывая каждого из них, так что вы остаетесь только с уникальным ключом, полем и столбцами имени поля для каждого. Затем вы присоединяетесь к двум и фильтруете все, что было изменено.

Вот полный рабочий пример, включая некоторые тестовые вызовы, чтобы показать, что регистрируется.

-- -------------------- Setup tables and some initial data -------------------- 
CREATE TABLE dbo.Sample_Table (ContactID int, Forename varchar(100), Surname varchar(100), Extn varchar(16), Email varchar(100), Age int); 
INSERT INTO Sample_Table VALUES (1,'Bob','Smith','2295','[email protected]',24); 
INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','[email protected]',32); 
INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','[email protected]',19); 
INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','[email protected]',28); 
INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','[email protected]',25); 

CREATE TABLE dbo.Sample_Table_Changes (ContactID int, FieldName sysname, FieldValueWas sql_variant, FieldValueIs sql_variant, modified datetime default (GETDATE())); 

GO 

-- -------------------- Create trigger -------------------- 
CREATE TRIGGER TriggerName ON dbo.Sample_Table FOR DELETE, INSERT, UPDATE AS 
BEGIN 
    SET NOCOUNT ON; 
    --Unpivot deleted 
    WITH deleted_unpvt AS (
     SELECT ContactID, FieldName, FieldValue 
     FROM 
      (SELECT ContactID 
       , cast(Forename as sql_variant) Forename 
       , cast(Surname as sql_variant) Surname 
       , cast(Extn as sql_variant) Extn 
       , cast(Email as sql_variant) Email 
       , cast(Age as sql_variant) Age 
      FROM deleted) p 
     UNPIVOT 
      (FieldValue FOR FieldName IN 
       (Forename, Surname, Extn, Email, Age) 
     ) AS deleted_unpvt 
    ), 
    --Unpivot inserted 
    inserted_unpvt AS (
     SELECT ContactID, FieldName, FieldValue 
     FROM 
      (SELECT ContactID 
       , cast(Forename as sql_variant) Forename 
       , cast(Surname as sql_variant) Surname 
       , cast(Extn as sql_variant) Extn 
       , cast(Email as sql_variant) Email 
       , cast(Age as sql_variant) Age 
      FROM inserted) p 
     UNPIVOT 
      (FieldValue FOR FieldName IN 
       (Forename, Surname, Extn, Email, Age) 
     ) AS inserted_unpvt 
    ) 

    --Join them together and show what's changed 
    INSERT INTO Sample_Table_Changes (ContactID, FieldName, FieldValueWas, FieldValueIs) 
    SELECT Coalesce (D.ContactID, I.ContactID) ContactID 
     , Coalesce (D.FieldName, I.FieldName) FieldName 
     , D.FieldValue as FieldValueWas 
     , I.FieldValue AS FieldValueIs 
    FROM 
     deleted_unpvt d 

      FULL OUTER JOIN 
     inserted_unpvt i 
      on  D.ContactID = I.ContactID 
       AND D.FieldName = I.FieldName 
    WHERE 
     D.FieldValue <> I.FieldValue --Changes 
     OR (D.FieldValue IS NOT NULL AND I.FieldValue IS NULL) -- Deletions 
     OR (D.FieldValue IS NULL AND I.FieldValue IS NOT NULL) -- Insertions 
END 
GO 
-- -------------------- Try some changes -------------------- 
UPDATE Sample_Table SET age = age+1; 
UPDATE Sample_Table SET Extn = '5'+Extn where Extn Like '221_'; 

DELETE FROM Sample_Table WHERE ContactID = 3; 

INSERT INTO Sample_Table VALUES (6,'Stephen','Turner','2299','[email protected]',25); 

UPDATE Sample_Table SET ContactID = 7 where ContactID = 4; --this will be shown as a delete and an insert 
-- -------------------- See the results -------------------- 
SELECT *, SQL_VARIANT_PROPERTY(FieldValueWas, 'BaseType') FieldBaseType, SQL_VARIANT_PROPERTY(FieldValueWas, 'MaxLength') FieldMaxLength from Sample_Table_Changes; 

-- -------------------- Cleanup -------------------- 
DROP TABLE dbo.Sample_Table; DROP TABLE dbo.Sample_Table_Changes; 

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

С другой стороны, выходной сигнал находится в другом формате, и все значения поля преобразуются в sql_variant, первый может быть исправлен путем поворота вывода снова, а второй может быть исправлен путем повторного возврата к требуемым типам на основе ваши знания о дизайне таблицы, но для обоих из них потребуется сложный динамический sql. Оба эти могут не быть проблемой в вашем XML-выходе.

Редактировать: Если у вас есть первичный первичный ключ, который может измениться, вы можете использовать этот метод. Вам просто нужно добавить столбец, который по умолчанию заполняется с помощью GUID с помощью функции NEWID(). Затем вы используете этот столбец вместо первичного ключа.

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

+0

Хорошее решение. Обратите внимание: поскольку триггер вызывается после отдельных операторов insert/update/delete, вам действительно нужно иметь дело с сценарием обновления. Интересно, могут ли быть какие-либо упрощения (для скорости). –

+1

@HermanSchoenfeld У вас есть проблема с производительностью? Помните: «... преждевременная оптимизация - это корень всего зла». (Дональд Кнут) –

+0

Я думаю, что есть проблема. Ваш триггер предполагает, что первичный ключ не изменился, когда это возможно. Таким образом, вам все равно нужно разделить случаи вставки/обновления/удаления, что означает, что вы можете просто игнорировать случаи вставки/удаления вообще. Для случая обновления вам необходимо присоединиться к вставке/удалению с помощью индекса строки. –

3

Я сделал это как простой «однострочный». Без использования, поворота, петель, многих переменных и т. Д., Что делает его похожим на процедурное программирование. SQL следует использовать для обработки наборов данных :-), решение:

DECLARE @sql as NVARCHAR(1024); 

select @sql = coalesce(@sql + ',' + quotename(column_name), quotename(column_name)) 
from INFORMATION_SCHEMA.COLUMNS 
where substring(columns_updated(), columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId')/8 + 1, 1) & power(2, -1 + columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') % 8) > 0 
    and table_name = 'DBCustomers' 
    -- and column_name in ('c1', 'c2') -- limit to specific columns 
    -- and column_name not in ('c3', 'c4') -- or exclude specific columns 

SET @sql = 'SELECT ' + @sql + ' FROM inserted FOR XML RAW'; 

DECLARE @x as XML; 
SET @x = CAST(EXEC(@sql) AS XML); 

Он использует COLUMNS_UPDATED, заботится о более чем восьми колонн - он обрабатывает столько столбцов, сколько вы хотите.

Он позаботится о правильном заказе колонок, который должен получить с помощью COLUMNPROPERTY.

Он основан на представлении COLUMNS, поэтому он может включать или исключать только определенные столбцы.

+1

Это будет работать, но динамический sql не имеет доступа к таблицам 'Inserted' или' Deleted'. – Fowl

+0

Inserted/Deleted псевдо таблицы резидентные не доступны во время выполнения динамического SQL , так что я бы выбрать * в #tmpInserted из вставлено выберите * в #tmpDeleted из удаленных и использовать # tmpInserted, # tmpDeleted в динамическом sql –

+0

Эта ссылка [MS support] (https://support.microsoft.com/en-us/kb/232195) указывает, что COLUMNS_UPDATED не работает более чем для 32 столбцов. Ваш код работает с 2008 R2 не менее чем на 62 столбца. – Maa421s

1

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

/******************************************************************************************* 
*   Add the below table to your database to track data changes using the trigger * 
*   below. Remember to change the variables in the trigger to match the table that * 
*   will be firing the trigger              * 
*******************************************************************************************/ 
SET ANSI_NULLS ON; 
GO 

SET QUOTED_IDENTIFIER ON; 
GO 

CREATE TABLE [dbo].[AuditDataChanges] 
(
    [RecordId] [INT] IDENTITY(1, 1) 
        NOT NULL , 
    [TableName] [VARCHAR](50) NOT NULL , 
    [RecordPK] [VARCHAR](50) NOT NULL , 
    [ColumnName] [VARCHAR](50) NOT NULL , 
    [OldValue] [VARCHAR](50) NULL , 
    [NewValue] [VARCHAR](50) NULL , 
    [ChangeDate] [DATETIME2](7) NOT NULL , 
    [UpdatedBy] [VARCHAR](50) NOT NULL , 
    CONSTRAINT [PK_AuditDataChanges] PRIMARY KEY CLUSTERED 
    ([RecordId] ASC) 
    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
      ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) 
ON [PRIMARY]; 

GO 

ALTER TABLE [dbo].[AuditDataChanges] ADD CONSTRAINT [DF_AuditDataChanges_ChangeDate] DEFAULT (GETDATE()) FOR [ChangeDate]; 
GO 



/************************************************************************************************ 
* Add the below trigger to any table you want to audit data changes on. Changes will be saved * 
* in the AuditChangesTable.                 * 
************************************************************************************************/ 


ALTER TRIGGER trg_Survey_Identify_Updated_Columns ON Survey --Change to match your table name 
    FOR INSERT, UPDATE 
AS 
SET NOCOUNT ON; 

DECLARE @sql VARCHAR(5000) , 
    @sqlInserted NVARCHAR(500) , 
    @sqlDeleted NVARCHAR(500) , 
    @NewValue NVARCHAR(100) , 
    @OldValue NVARCHAR(100) , 
    @UpdatedBy VARCHAR(50) , 
    @ParmDefinitionD NVARCHAR(500) , 
    @ParmDefinitionI NVARCHAR(500) , 
    @TABLE_NAME VARCHAR(100) , 
    @COLUMN_NAME VARCHAR(100) , 
    @modifiedColumnsList NVARCHAR(4000) , 
    @ColumnListItem NVARCHAR(500) , 
    @Pos INT , 
    @RecordPk VARCHAR(50) , 
    @RecordPkName VARCHAR(50); 

SELECT * 
INTO #deleted 
FROM deleted; 
SELECT * 
INTO #Inserted 
FROM inserted; 

SET @TABLE_NAME = 'Survey'; ---Change to your table name 
SELECT @UpdatedBy = UpdatedBy --Change to your column name for the user update field 
FROM inserted; 
SELECT @RecordPk = SurveyId --Change to the table primary key field 
FROM inserted; 
SET @RecordPkName = 'SurveyId'; 
SET @modifiedColumnsList = STUFF((SELECT ',' + name 
            FROM  sys.columns 
            WHERE object_id = OBJECT_ID(@TABLE_NAME) 
              AND SUBSTRING(COLUMNS_UPDATED(), 
                  ((column_id 
                  - 1)/8 + 1), 
                  1) & (POWER(2, 
                  ((column_id 
                  - 1) % 8 + 1) 
                  - 1)) = POWER(2, 
                  (column_id - 1) 
                  % 8) 
           FOR 
            XML PATH('') 
           ), 1, 1, ''); 


WHILE LEN(@modifiedColumnsList) > 0 
    BEGIN 
     SET @Pos = CHARINDEX(',', @modifiedColumnsList); 
     IF @Pos = 0 
      BEGIN 
       SET @ColumnListItem = @modifiedColumnsList; 
      END; 
     ELSE 
      BEGIN 
       SET @ColumnListItem = SUBSTRING(@modifiedColumnsList, 1, 
               @Pos - 1); 
      END;  

     SET @COLUMN_NAME = @ColumnListItem; 
     SET @ParmDefinitionD = N'@OldValueOut NVARCHAR(100) OUTPUT'; 
     SET @ParmDefinitionI = N'@NewValueOut NVARCHAR(100) OUTPUT'; 
     SET @sqlDeleted = N'SELECT @OldValueOut=' + @COLUMN_NAME 
      + ' FROM #deleted where ' + @RecordPkName + '=' 
      + CONVERT(VARCHAR(50), @RecordPk); 
     SET @sqlInserted = N'SELECT @NewValueOut=' + @COLUMN_NAME 
      + ' FROM #Inserted where ' + @RecordPkName + '=' 
      + CONVERT(VARCHAR(50), @RecordPk); 
     EXECUTE sp_executesql @sqlDeleted, @ParmDefinitionD, 
      @OldValueOut = @OldValue OUTPUT; 
     EXECUTE sp_executesql @sqlInserted, @ParmDefinitionI, 
      @NewValueOut = @NewValue OUTPUT; 
     IF (LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue))) 
      BEGIN 
       SET @sql = 'INSERT INTO [dbo].[AuditDataChanges] 
               ([TableName] 
               ,[RecordPK] 
               ,[ColumnName] 
               ,[OldValue] 
               ,[NewValue] 
               ,[UpdatedBy]) 
             VALUES 
               (' + QUOTENAME(@TABLE_NAME, '''') + ' 
               ,' + QUOTENAME(@RecordPk, '''') + ' 
               ,' + QUOTENAME(@COLUMN_NAME, '''') + ' 
               ,' + QUOTENAME(@OldValue, '''') + ' 
               ,' + QUOTENAME(@NewValue, '''') + ' 
               ,' + QUOTENAME(@UpdatedBy, '''') + ')'; 


       EXEC (@sql); 
      END;  
     SET @COLUMN_NAME = ''; 
     SET @NewValue = ''; 
     SET @OldValue = ''; 
     IF @Pos = 0 
      BEGIN 
       SET @modifiedColumnsList = ''; 
      END; 
     ELSE 
      BEGIN 
      -- start substring at the character after the first comma 
       SET @modifiedColumnsList = SUBSTRING(@modifiedColumnsList, 
                @Pos + 1, 
                LEN(@modifiedColumnsList) 
                - @Pos); 
      END; 
    END; 
DROP TABLE #Inserted; 
DROP TABLE #deleted; 

GO 
Смежные вопросы