2009-08-10 115 views
12

我知道COLUMNS_UPDATED,以及我需要一些快捷方式(如果有人已经提出,我已经做出了一个,但如果有人可以节省我的时间,我会appriciate它)SQL Server更新触发器,获取只修改的字段

我需要basicaly只更新的列值的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 

回答

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 

但是如果你有超过62列的表这个snipet的代码失败..阿瑟。溢出...

这是处理多于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

感谢您的回答,但该连接是在这个级别考虑了巨大的变化确实昂贵的,所以我不打算使用它,因为我试图发现在更改跟踪,但由于东西反正。 – 2009-12-17 16:59:18

+0

不错,但是对于信息来说,当sys.columns中的column_id存在空隙时(上面的代码被丢弃的列),上面的代码没有检测到它。 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 2016-01-22 12:06:24

+0

它没有按预期工作。有时它适用于1个字段,有时可用,有时完全失败(它基​​于查询,但不是同一查询的随机输出)。我正在使用SQL Server 2014(120)! – SKLTFZ 2017-01-04 05:30:23

1

的唯一方式发生,我认为你可以做到这一点,而不硬编码列名称将被删除表的内容拖放到一个临时表,然后建立基于表的定义来比较你的临时表与实际表格的内容的查询,并返回基于他们是否做什么或不匹配分隔列的列表。无可否认,以下内容是精心制作的。

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位域和ARTH溢出问题瞎搞。如果您在设计时知道要比较的列,那么您不需要任何动态SQL。

缺点是输出格式不同,所有字段值都转换为sql_variant,第一个可以通过再次旋转输出来修复,第二个可以通过基于你对表的设计知识,但这两个都需要一些复杂的动态SQL。这两个可能不是您的XML输出中的问题。

编辑:回顾下面的评论,如果你有一个自然的主键可以改变,那么你仍然可以使用这种方法。您只需添加一个使用NEWID()函数默认填充GUID的列。然后,您使用此列代替主键。

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

+0

不错的解决方案。请注意,由于在单独的插入/更新/删除语句之后调用触发器,您只需要处理更新方案。想知道是否可以有任何简化(速度)。 – 2013-03-14 05:38:39

+1

@HermanSchoenfeld您是否有性能问题?记住“过早优化是万恶之源。” (唐纳德克努特) – 2013-03-14 22:31:34

+0

我觉得有一个问题。你的触发器假定主键没有改变。所以,无论如何,你需要分离插入/更新/删除的情况,这意味着你可以完全忽略插入/删除的情况。对于更新情况,您需要连接插入/删除行索引。 – 2013-03-18 02:25:41

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 2014-08-29 05:05:31

+0

在插入/删除动态SQL的执行过程中是伪内存驻留表不可用 ,所以我会选择 *成从#tmpInserted插入 选择*到从删除 #tmpDeleted并使用#tmpInserted,#tmpDeleted在动态sql – 2015-09-22 16:50:33

+0

此链接[MS支持](https://support.microsoft.com/en-us/kb/232195)表示COLUMNS_UPDATED不适用于32列以上。您的代码至少在62列上适用于2008 R2。 – Maa421s 2015-11-18 18:11:40

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