【问题标题】:SQL Server Update Trigger, Get Only modified fieldsSQL Server 更新触发器,只获取修改的字段
【发布时间】:2010-11-18 07:03:42
【问题描述】:

我知道COLUMNS_UPDATED,我需要一些快速的捷径(如果有人做过,我已经在做,但如果有人能节省我的时间,我会感谢它)

我需要一个只包含更新列值的 XML,我需要这个用于复制目的。

SELECT * FROM inserted 给我每一列,但我只需要更新的。

类似跟随...

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

【问题讨论】:

    标签: sql sql-server triggers sql-update database-replication


    【解决方案1】:

    我有另一个完全不同的解决方案,它根本不使用 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','bs@example.com',24);
    INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','ab@example.com',32);
    INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','rj@example.com',19);
    INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','md@example.com',28);
    INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','pn@example.com',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','st@example.com',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 位域和arth 溢出问题。如果您在设计时知道要比较的列,那么您不需要任何动态 SQL。

    不利的一面是,输出格式不同,所有字段值都转换为 sql_variant,第一个可以通过再次旋转输出来修复,第二个可以通过根据您的重新转换为所需类型来修复了解表的设计,但这两者都需要一些复杂的动态sql。这两个在您的 XML 输出中可能都不是问题。这个question 的作用类似于以相同的格式恢复输出。

    编辑:查看下面的 cmets,如果您有一个可以更改的自然主键,那么您仍然可以使用此方法。您只需要使用 NEWID() 函数添加默认填充有 GUID 的列。然后使用此列代替主键。

    您可能希望为此字段添加索引,但由于触发器中的已删除和插入表在内存中,因此它可能不会被使用并且可能会对性能产生负面影响。

    【讨论】:

    • 不错的解决方案。请注意,由于在单独的插入/更新/删除语句之后调用了触发器,因此您只需要处理更新场景。想知道是否可以进行任何简化(为了速度)。
    • @HermanSchoenfeld 您有性能问题吗?记住“......过早的优化是万恶之源。” (唐纳德·克努斯)
    • 我认为有问题。您的触发器假定主键没有更改,如果可以的话。因此,无论如何,您都需要分离出插入/更新/删除案例,这意味着您可以完全忽略插入/删除案例。对于更新情况,您需要加入由行索引插入/删除。
    • @HermanSchoenfeld 是的,此代码假定 surrogate primary key as opposed to a natural 可能会更改。但是,如果您的密钥可以更改,您怎么知道它是更改的行而不是新行?我认为您对 OP 有不同的问题。
    • 我正在讨论的问题与 OP 相同,只是要知道后触发器中的所有实际更改。您的解决方案在更改代理键和 BLOB、TEXT、XML 字段时失败(因为它们无法转换为 sql_variant)。除了 Blob 字段,识别行的正确方法不是通过它的代理键,而是通过它在插入/删除的临时表中的索引。插入/删除的 UPDATE AFTER 触发器具有 1-1 双射。这就是为什么在触发器之后插入和删除在这里根本不重要。
    【解决方案2】:

    在触发器内部,您可以像这样使用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
    

    但是当你有一个超过 62 列的表时,这段代码会失败.. Arth.Overflow...

    这是处理超过 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
    

    【讨论】:

    • 感谢您的回答,但是考虑到巨大的变化,在这个级别上加入真的很昂贵,所以我不会使用它,因为我试图在变化跟踪中发现一些东西,但无论如何谢谢。跨度>
    • 很好,但对于信息,当 sys.columns 中的 column_id 存在间隙时(例如删除的列),上面的代码不会检测到它。例如,有 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
    • 它没有按预期工作。有时它适用于 1 个字段,有时它有效,有时它完全失败(它基​​于查询,但不是同一查询的随机输出)。我正在使用 SQL Server 2014(120)!
    • 我想明确一点,COLUMNS_UPDATED NOT 有 62 列的限制 - 该字段中的数据是 varbinary(max)这是 2^31-1 个字节
    【解决方案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,因此它可能只包含或排除特定列。

    【讨论】:

    • 这可行,但动态 sql 无权访问 InsertedDeleted 表。
    • Inserted/Deleted 是在执行动态 sql 期间不可用的伪内存驻留表,因此我将 select * into #tmpInserted from inserted select * into #tmpDeleted from deleted 并在您的动态sql
    • 此链接MS support 表示 COLUMNS_UPDATED 不适用于超过 32 列。您的代码在至少 62 列上与 2008 R2 一起工作。
    【解决方案4】:

    以下代码适用于超过 64 列,并且仅记录更新的列。按照 cmets 中的说明进行操作,一切都会好起来的。

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

    【讨论】:

      【解决方案5】:

      我将接受的答案转换为以逗号分隔的列名列表(根据作者的建议)。输出 - “Columns_Updated”为 'Column1,Column2,Column5'

      -- get names of updated columns
      DECLARE @idTable      INT
      declare @ColumnName nvarchar(300)
      declare @ColId int
      
      SELECT  @idTable = T.id 
      FROM    sysobjects P JOIN sysobjects T ON P.parent_obj = T.id 
      WHERE   P.id = @@procid
      
      DECLARE @changedProperties nvarchar(max) = ''
      
      DECLARE @Columns_Updated VARCHAR(2000) = ''
      
      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 BEGIN
                  SET @ColId = cast( CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) as int)
      
                  select @ColumnName = [Name]
                  FROM syscolumns 
                  WHERE id = @idTable and colid = @ColId
      
                  SET @Columns_Updated = @Columns_Updated + ',' + @ColumnName
              END
              SET @curBit = @curBit + 1
          END
          SET @curByteCU = @curByteCU + 1
      END
      

      【讨论】:

        【解决方案6】:

        在我看来,无需硬编码列名即可完成此操作的唯一方法是将已删除表的内容拖放到临时表中,然后基于表定义构建查询以比较您的临时表和实际表,并根据它们是否匹配返回分隔列列表。诚然,以下是详尽的。

        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
        

        【讨论】:

          【解决方案7】:

          Rick 提供的示例代码缺少对多行更新的处理。

          请让我增强 Rick 的版本如下:

          USE [AFC]
          GO
          
          /****** Object:  Trigger [dbo].[trg_Survey_Identify_Updated_Columns]    Script Date: 27/7/2018 14:08:49 ******/
          SET ANSI_NULLS ON
          GO
          
          SET QUOTED_IDENTIFIER ON
          GO
          
          ALTER TRIGGER [dbo].[trg_Survey_Identify_Updated_Columns] ON [dbo].[Sample_Table] --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 = 'Sample_Table';---Change to your table name
          
          DECLARE t_cursor CURSOR
          FOR
          SELECT ContactID 
          FROM inserted
          
          OPEN t_cursor
          
          FETCH NEXT
          FROM t_cursor
          INTO @RecordPk 
          
          WHILE @@FETCH_STATUS = 0
          BEGIN
          	--SELECT @UpdatedBy = Surname --Change to your column name for the user update field
          	--FROM inserted;
          	--SELECT @RecordPk = ContactID --Change to the table primary key field
          	--FROM inserted;
          	SET @RecordPkName = 'ContactID';
          	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;
          
          		--PRINT @newvalue
          		--PRINT @oldvalue
          
          		IF (LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue)))
          		BEGIN
          			SET @sql = 'INSERT INTO [dbo].[AuditDataChanges]
                                                         ([TableName]
                                                         ,[RecordPK]
                                                         ,[ColumnName]
                                                         ,[OldValue]
                                                         ,[NewValue] )
                                                   VALUES
                                                         (' + QUOTENAME(@TABLE_NAME, '''') + '
                                                         ,' + QUOTENAME(@RecordPk, '''') + '
                                                         ,' + QUOTENAME(@COLUMN_NAME, '''') + '
                                                         ,' + QUOTENAME(@OldValue, '''') + '
                                                         ,' + QUOTENAME(@NewValue, '''') + '
                                                         '  + ')';
          
          			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;
          
          	FETCH NEXT
          	FROM t_cursor
          	INTO @RecordPk 
          END
          
          DROP TABLE #Inserted;
          
          DROP TABLE #deleted;
          
          CLOSE t_cursor;
          
          DEALLOCATE t_cursor;

          【讨论】:

            【解决方案8】:

            这是使用唯一记录和 UpdatedBy 用户跟踪更新列值的完美示例。

            IF NOT EXISTS
                  (SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[ColumnAuditLogs]') 
                           AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
                   CREATE TABLE ColumnAuditLogs
                           (Type CHAR(1), 
                           TableName VARCHAR(128), 
                           PK VARCHAR(1000), 
                           FieldName VARCHAR(128), 
                           OldValue VARCHAR(1000), 
                           NewValue VARCHAR(1000), 
                           UpdateDate datetime, 
                           UserName VARCHAR(128),
                           UniqueId uniqueidentifier,
                           UpdatedBy int
                           )
            GO
            
            create TRIGGER TR_ABCTable_AUDIT ON ABCTable FOR UPDATE
            AS
            
            DECLARE @bit INT ,
                   @field INT ,
                   @maxfield INT ,
                   @char INT ,
                   @fieldname VARCHAR(128) ,
                   @TableName VARCHAR(128) ,
                   @PKCols VARCHAR(1000) ,
                   @sql VARCHAR(2000), 
                   @UpdateDate VARCHAR(21) ,
                   @UserName VARCHAR(128) ,
                   @Type CHAR(1) ,
                   @PKSelect VARCHAR(1000),
                   @UniqueId varchar(100),
                   @UpdatedBy VARCHAR(50) 
            
            
            --You will need to change @TableName to match the table to be audited. 
            -- Here we made ABCTable for your example.
            SELECT @TableName = 'ABCTable' -- change table name accoring your table name
            
            -- use for table unique records for everytime updation.
            set @UniqueId = CONVERT(varchar(100),newID())
            -- date and user
            SELECT         @UserName = SYSTEM_USER ,
                   @UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
            
             SELECT  @UpdatedBy = ModifiedBy --Change to your column name for the user update field
            FROM    inserted;
            
            
            -- Action
            IF EXISTS (SELECT * FROM inserted)
                   IF EXISTS (SELECT * FROM deleted)
                           SELECT @Type = 'U'
                   ELSE
                           SELECT @Type = 'I'
            ELSE
                   SELECT @Type = 'D'
            
            -- get list of columns
            SELECT * INTO #ins FROM inserted
            SELECT * INTO #del FROM deleted
            
            -- Get primary key columns for full outer join
            SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
                           + ' i.' + c.COLUMN_NAME + ' = d.' + c.COLUMN_NAME
                   FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
            
                          INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
                   WHERE   pk.TABLE_NAME = @TableName
                   AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
                   AND     c.TABLE_NAME = pk.TABLE_NAME
                   AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
            
            
            -- Get primary key select for insert
            SELECT @PKSelect = COALESCE(@PKSelect+'+','') 
                   + 'convert(varchar(100),
            coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))' 
                   FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
                           INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
                   WHERE   pk.TABLE_NAME = @TableName
                   AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
                   AND     c.TABLE_NAME = pk.TABLE_NAME
                   AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
            
            IF @PKCols IS NULL
            BEGIN
                   RAISERROR('no PK on table %s', 16, -1, @TableName)
                   RETURN
            END
            
            SELECT         @field = 0, 
                   @maxfield = MAX(ORDINAL_POSITION) 
                   FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName
            WHILE @field < @maxfield
            BEGIN
                   SELECT @field = MIN(ORDINAL_POSITION) 
                           FROM INFORMATION_SCHEMA.COLUMNS 
                           WHERE TABLE_NAME = @TableName 
                           AND ORDINAL_POSITION > @field
                   SELECT @bit = (@field - 1 )% 8 + 1
                   SELECT @bit = POWER(2,@bit - 1)
                   SELECT @char = ((@field - 1) / 8) + 1
                   IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0
                                                   OR @Type IN ('I','D')
                   BEGIN
                           SELECT @fieldname = COLUMN_NAME 
                                   FROM INFORMATION_SCHEMA.COLUMNS 
                                   WHERE TABLE_NAME = @TableName 
                                   AND ORDINAL_POSITION = @field
                           SELECT @sql = '
            insert ColumnAuditLogs (    Type, 
                           TableName, 
                           PK, 
                           FieldName, 
                           OldValue, 
                           NewValue, 
                           UpdateDate, 
                           UserName,
                           UniqueId,
                           [UpdatedBy])
            select ''' + @Type + ''',''' 
                   + @TableName + ''',' + @PKSelect
                   + ',''' + @fieldname + ''''
                   + ',convert(varchar(1000),d.' + @fieldname + ')'
                   + ',convert(varchar(1000),i.' + @fieldname + ')'
                   + ',''' + @UpdateDate + ''''
                   + ',''' + @UserName + ''''
                   + ',''' + @UniqueId + ''''
                    + ',' + QUOTENAME(@UpdatedBy, '''')
                   + ' from #ins i full outer join #del d'
                   + @PKCols
                   + ' where i.' + @fieldname + ' <> d.' + @fieldname 
                   + ' or (i.' + @fieldname + ' is null and  d.'
                                            + @fieldname
                                            + ' is not null)' 
                   + ' or (i.' + @fieldname + ' is not null and  d.' 
                                            + @fieldname
                                            + ' is null)' 
                           EXEC (@sql)
                   END
            END
            
            GO
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-08-01
              • 2020-03-24
              • 2019-07-06
              • 1970-01-01
              • 2011-06-03
              • 1970-01-01
              相关资源
              最近更新 更多