代码之家  ›  专栏  ›  技术社区  ›  Akash Kava

SQL Server更新触发器,仅获取已修改的字段

  •  15
  • Akash Kava  · 技术社区  · 16 年前

    我知道 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
    
    6 回复  |  直到 7 年前
        1
  •  14
  •   Alexander Abakumov    9 年前

    在触发器内部,您可以使用 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列的表时,这个代码片段会失败。啊,溢出…

    这里是最终版本,它处理超过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
    
        2
  •  19
  •   Stephen Turner    7 年前

    我有另一个完全不同的解决方案,它不使用任何更新的列,也不依赖于在运行时构建动态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_变量,第一个字段值可以通过再次旋转输出来固定,第二个字段值可以通过根据您对表设计的了解重新复制到所需类型来固定,但这两个字段值都需要一些复杂的动态sql。这两个问题在XML输出中可能都不是问题。这个 question 执行类似于以相同格式返回输出的操作。

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

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

        3
  •  4
  •   Michał Powaga Mohamed Aslam    12 年前

    我只做了简单的“一行”。不使用、透视、循环、许多变量等,使其看起来像程序化编程。应使用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 ,处理8个以上的列-它可以处理任意多个列。

    它注意正确的列顺序,应该使用 COLUMNPROPERTY .

    它是基于视图的 COLUMNS 因此,它可能只包括或排除特定的列。

        4
  •  1
  •   cmsjr    16 年前

    在没有硬编码列名的情况下,我唯一能做到这一点的方法是将已删除表的内容放到临时表中,然后根据表定义构建一个查询,以比较临时表和实际表的内容,并根据列列表的分隔与否返回一个列列表。比赛。诚然,下面是详细的。

    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
    
        5
  •  1
  •   Rick    8 年前

    下面的代码适用于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
    
        6
  •  0
  •   RAY    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;