SQL Server Update Trigger, Get Only modified fields

前端 未结 8 1203
旧巷少年郎
旧巷少年郎 2020-11-29 03:44

I am aware of COLUMNS_UPDATED, well I need some quick shortcut (if anyone has made, I am already making one, but if anyone can save my time, I will appriciate i

相关标签:
8条回答
  • 2020-11-29 04:02

    Inside the trigger, you can use COLUMNS_UPDATED() like this in order to get updated value

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

    But this snipet of code fails when you have a table with more than 62 columns.. Arth.Overflow...

    Here is the final version which handles more than 62 columns but give only the number of the updated columns. It's easy to link with 'syscolumns' to get the name

    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)
  • 2020-11-29 04:03

    I've done it as simple "one-liner". Without using, pivot, loops, many variables etc. that makes it looking like procedural programming. SQL should be used to process data sets :-), the solution is:

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

    It uses COLUMNS_UPDATED, takes care of more than eight columns - it handles as many columns as you want.

    It takes care on proper columns order which should be get using COLUMNPROPERTY.

    It is based on view COLUMNS so it may include or exclude only specific columns.

    0 讨论(0)
  • 2020-11-29 04:03

    I transformed the accepted answer to get list of column names separated by comma (according to author's recommendation). Output - "Columns_Updated" as '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
    
    0 讨论(0)
  • 2020-11-29 04:08

    I've another completely different solution that doesn't use COLUMNS_UPDATED at all, nor does it rely on building dynamic SQL at runtime. (You might want to use dynamic SQL at design time but thats another story.)

    Basically you start with the inserted and deleted tables, unpivot each of them so you are just left with the unique key, field value and field name columns for each. Then you join the two and filter for anything that's changed.

    Here is a full working example, including some test calls to show what is logged.

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

    So no messing around with bigint bitfields and arth overflow problems. If you know the columns you want to compare at design time then you don't need any dynamic SQL.

    On the downside the output is in a different format and all the field values are converted to sql_variant, the first could be fixed by pivoting the output again, and the second could be fixed by recasting back to the required types based on your knowledge of the design of the table, but both of these would require some complex dynamic sql. Both of these might not be an issue in your XML output. This question does something similar to getting the output back in the same format.

    Edit: Reviewing the comments below, if you have a natural primary key that could change then you can still use this method. You just need to add a column that is populated by default with a GUID using the NEWID() function. You then use this column in place of the primary key.

    You may want to add an index to this field, but as the deleted and inserted tables in a trigger are in memory it might not get used and may have a negative effect on performance.

    0 讨论(0)
  • 2020-11-29 04:13

    This is perfect example for track log of updated columnwise value with unique records and UpdatedBy user.

    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
    
    0 讨论(0)
  • 2020-11-29 04:18

    The below code works for over 64 columns and logs only the updated columns. Follow the instruction in the comments and all should be well.

    /*******************************************************************************************
     *         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
    
    0 讨论(0)
提交回复
热议问题