带触发器的Delta跟踪

时间:2016-08-29 09:18:34

标签: sql sql-server triggers sql-server-2012

我尝试根据触发器为多个表创建Delta跟踪。我正在使用sql server 2012。 为了简化我的问题,我想仅查看更新后触发器。

所以这是跟踪表:

CREATE TABLE [dbo].[History](
    [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_History_Id]  DEFAULT (newsequentialid()),
    [ObjectId] [uniqueidentifier] NOT NULL,
    [Timestamp] [datetime] NOT NULL CONSTRAINT [DF_History_Timestamp]  DEFAULT (getdate()),
    [ChangingUser] [varchar](max) NOT NULL CONSTRAINT [DF_History_ChangingUser]  DEFAULT (suser_sname()),
    [Column] [varchar](max) NOT NULL,
    [OldValue] [nvarchar](max) NULL,
    [NewValue] [nvarchar](max) NULL,
 CONSTRAINT [PK_History] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

现在我要在表上创建一个触发器,我想跟踪它:

CREATE TRIGGER [dbo].[T1_TraceUpdate]
   on [dbo].[T1]
   AFTER update
AS 
BEGIN
set nocount on;
-- pseudo insert!
insert into History select * from inserted;
END

除了与历史记录表不匹配的布局外,我在确定已更改且未触及的值时遇到问题。 有没有办法以更聪明的方式做到这一点,而不仅仅使用 if(updated(col_name))的dozends? 另外,我必须记住,可能有多行受到影响。 是否有更好,更可恢复的解决方案(甚至可能没有游标?)......

编辑#1:T1和历史记录的示例数据

T1设计:

CREATE TABLE [dbo].[T1](
    [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_T1_Id]  DEFAULT (newsequentialid()),
    [Title] [text] NULL,
    [Amount] [int] NULL,
    [Price] [decimal](18, 7) NULL,
    [Version] [timestamp] NOT NULL,
 CONSTRAINT [PK_T1] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

示例数据可能是这样的:

1d118497-bf69-e611-9e7d-40a8f04d1095    Abc            3    37,2500000  
9cf095a8-bf69-e611-9e7d-40a8f04d1095    NULL           1    27,3000000  
cc38386d-fe69-e611-9e7d-40a8f04d1095    Storm Catcher  10   NULL    

让我们发布一个愚蠢的更新:

update T1 set price = isnull(Price,100)*0.7 where Amount > 2

历史现在应该是这样的:

4848D80B-4E73-E611-BD43-40A8F04D1095    1D118497-BF69-E611-9E7D-40A8F04D1095    2016-09-05 11:49:33.473 sa  Price   37,2500000  26.0750000
E80EAB18-4E73-E611-BD43-40A8F04D1095    CC38386D-FE69-E611-9E7D-40A8F04D1095    2016-09-05 11:49:33.473 sa  Price   NULL        70

5 个答案:

答案 0 :(得分:3)

触发器有inserted and deleted个表,可用于

  

找出a之前和之后表的状态之间的差异   数据修改并根据这种差异采取行动。

     

更新事务类似于删除操作后跟一个   插入操作;先将旧行复制到已删除的表中,   然后将新行复制到触发器表和   插表。

取消隐藏这些表以使每列有一行,比较它们并仅在History表中插入旧值和新值不同的行。

CREATE TRIGGER [dbo].[T1_TraceUpdate]
    on [dbo].[T1]
    AFTER update
AS 
BEGIN
    set nocount on;

    WITH
    CTE_Inserted
    AS
    (
        SELECT ID, ColumnName, ColumnValue
        FROM
            inserted
            CROSS APPLY
            (VALUES
                ('Title', Title),
                ('Amount', CAST(Amount AS nvarchar(max))),
                ('Price', CAST(Price AS nvarchar(max)))
            ) AS V(ColumnName, ColumnValue)
    )
    ,CTE_Deleted
    AS
    (
        SELECT ID, ColumnName, ColumnValue
        FROM
            deleted
            CROSS APPLY
            (VALUES
                ('Title', Title),
                ('Amount', CAST(Amount AS nvarchar(max))),
                ('Price', CAST(Price AS nvarchar(max)))
            ) AS V(ColumnName, ColumnValue)
    )
    INSERT INTO dbo.History([ObjectId], [Column], [OldValue], [NewValue])
    SELECT
        CTE_Inserted.Id AS [ObjectId]
        ,CTE_Inserted.ColumnName
        ,CTE_Deleted.ColumnValue AS [OldValue]
        ,CTE_Inserted.ColumnValue AS [NewValue]
    FROM
        CTE_Inserted
        INNER JOIN CTE_Deleted
            ON  CTE_Deleted.Id = CTE_Inserted.Id
            AND CTE_Deleted.ColumnName = CTE_Inserted.ColumnName
    WHERE
        ISNULL(CTE_Inserted.ColumnValue, N'') <> ISNULL(CTE_Deleted.ColumnValue, N'')
        OR (CTE_Inserted.ColumnValue IS NULL AND CTE_Deleted.ColumnValue IS NOT NULL) 
        OR (CTE_Inserted.ColumnValue IS NOT NULL AND CTE_Deleted.ColumnValue IS NULL) 
    ;

END

您必须在nvarchar(max)表中使用text类型而不是T1。并且您必须将所有值转换/转换为nvarchar(max)(或者在History表中为不同类型设置多个列。)

答案 1 :(得分:3)

Jaster,最后我得到了一个查询解决方案。

它是一个动态脚本,您可以在其中提及所需表的名称。该脚本将通过创建动态触发器来管理所有列中所提到的表表上的数据更改(除了具有数据类型text,ntext和image的列),这些触发器将对这些表上的INSERT/UPDATE/DELETE操作触发。您可以根据自己的要求更改操作。我还在 HISTORY 表格中添加了TableNameModifiedDate,以便于查看。

以下是解决方案的步骤。

使用提供的信息

创建样本测试环境
IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'History')
CREATE TABLE [dbo].[History](
    [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_History_Id]  DEFAULT (newsequentialid()),
    [Tablename] VARCHAR(150),
    [ObjectId] VARCHAR(1500) NOT NULL,
    [Timestamp] [datetime] NOT NULL CONSTRAINT [DF_History_Timestamp]  DEFAULT (getdate()),
    [ChangingUser] [varchar](max) NOT NULL CONSTRAINT [DF_History_ChangingUser]  DEFAULT (suser_sname()),
    [Column] [varchar](max) NOT NULL,
    [OldValue] [nvarchar](max) NULL,
    [NewValue] [nvarchar](max) NULL,
       [ModifiedDate] Datetime NULL
CONSTRAINT [PK_History] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'T1')

BEGIN
CREATE TABLE [dbo].[T1](
    [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_T1_Id]  DEFAULT (newsequentialid()),
    [Title] text NULL,
    [Amount] [int] NULL,
    [Price] [decimal](18, 7) NULL,
    [Version] [datetime] NOT NULL,
CONSTRAINT [PK_T1] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] 

INSERT INTO [T1] ([Title],[Amount],[Price],[Version]) VALUES ('Abc',3,372500000 ,'2016-09-01 11:49:33.473'),
                                         (NULL,1,273000000  ,'2016-09-02 11:49:33.473'),
                                         ('Storm Catcher',10,NULL,'2016-09-01 11:49:33.473')
END

在下面的临时表中,您必须插入要跟踪数据更改的表名。

IF OBJECT_ID('tempdb..#FiltedTableList') IS NOT NULL
DROP TABLE #FiltedTableList
GO

CREATE TABLE #FiltedTableList
( ID INT IDENTITY(1,1),
  TableName VARCHAR(150))
INSERT INTO #FiltedTableList (TableName)
SELECT 'T1' --here we have only one table to track the changes

这是最后一步。执行以下动态查询。

DECLARE @TableName sysname,@Counter INT,@Dynamsql varchar(MAX) 
SELECT @Counter=MAX(ID) FROM #FiltedTableList

SET NOCOUNT ON

WHILE (@Counter !=0)

BEGIN

       SELECT @TableName= TableName
       FROM #FiltedTableList
       WHERE ID=@Counter

        DECLARE  @ColumnFilter VARCHAR (MAX)

        SELECT  @ColumnFilter=Stuff((   Select ', ' + C.COLUMN_NAME 
        From INFORMATION_SCHEMA.COLUMNS As C  Where  C.TABLE_NAME = @TableName
               AND c.DATA_TYPE NOT IN ('text','ntext','image')
                Order By C.ORDINAL_POSITION   For Xml Path('')  ), 1, 2, '')


        IF OBJECT_ID('tempdb..##MagInserted') IS NOT NULL
        DROP TABLE ##MagInserted

        IF OBJECT_ID('tempdb..##MagDeleted') IS NOT NULL
        DROP TABLE ##MagDeleted


EXEC('IF OBJECT_ID (''' + @TableName+ '_LogTracker'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TableName+ '_LogTracker')
SELECT @Dynamsql =
 'CREATE TRIGGER ' + @TableName+ '_LogTracker ON ' + @TableName+ ' FOR INSERT, UPDATE, DELETE
  AS
  DECLARE   @column int , @maxColumn int , @char int,@columnname varchar(128) , @TableName varchar(128) , @KeyColumn varchar(1000) , @Dynamsql varchar(2000) ,@dataquery VARCHAR(MAX), 
            @ModifiedDate varchar(21), @UserName varchar(128) , @key int , @Type char(1) , @PKFieldSelect varchar(1000),@PKValueSelect varchar(1000)
  SELECT @TableName = ''' + @TableName+ ''';
  SELECT @UserName = system_user , @ModifiedDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114);
  SELECT '+ @ColumnFilter+' INTO ##MagInserted FROM inserted;SELECT '+ @ColumnFilter+' INTO ##MagDeleted FROM deleted;
   SELECT @KeyColumn = COALESCE(@KeyColumn + '' and'', '' on'') + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME
   FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS kc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE c ON c.TABLE_NAME = kc.TABLE_NAME and c.CONSTRAINT_NAME = kc.CONSTRAINT_NAME
  WHERE kc.TABLE_NAME = @TableName AND CONSTRAINT_TYPE = ''PRIMARY KEY''
  SELECT @PKFieldSelect = COALESCE(@PKFieldSelect+''+'','''') + '''''''' + COLUMN_NAME + ''''''''
   FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS kc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE c ON c.TABLE_NAME = kc.TABLE_NAME and c.CONSTRAINT_NAME = kc.CONSTRAINT_NAME
  WHERE kc.TABLE_NAME = @TableName AND CONSTRAINT_TYPE = ''PRIMARY KEY''
  SELECT @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''convert(varchar(100), coalesce(i.'' + COLUMN_NAME + '',d.'' + COLUMN_NAME + ''))''
  FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS kc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE c ON c.TABLE_NAME = kc.TABLE_NAME  and c.CONSTRAINT_NAME = kc.CONSTRAINT_NAME
  WHERE  kc.TABLE_NAME = @TableName  AND CONSTRAINT_TYPE = ''PRIMARY KEY''  
  SELECT @column = 0 , @maxColumn = max(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName 
              WHILE @column < @maxColumn
              BEGIN
                     SELECT @column = min(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND ORDINAL_POSITION > @column
                     SELECT @key = (@column - 1 )% 8 + 1; SELECT @key = power(2,@key - 1);SELECT @char = ((@column - 1) / 8) + 1
                   IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @key > 0 OR @Type IN (''I'',''D'')
                           BEGIN
                           SELECT @columnname = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION = @column
                           SELECT @Dynamsql = ''insert History (TableName,[ObjectId], [Column], OldValue, NewValue, ModifiedDate, ChangingUser)''
                           SELECT @Dynamsql = @Dynamsql + '' select '''''' + @TableName + '''''''' 
                           SELECT @Dynamsql = @Dynamsql +  '','' + @PKValueSelect
                           SELECT @Dynamsql = @Dynamsql + '','''''' + @columnname + ''''''''
                           SELECT @Dynamsql = @Dynamsql + '',convert(varchar(1000),d.'' + @columnname + '')''
                           SELECT @Dynamsql = @Dynamsql + '',convert(varchar(1000),i.'' + @columnname + '')''
                           SELECT @Dynamsql = @Dynamsql + '','''''' + @ModifiedDate + ''''''''
                           SELECT @Dynamsql = @Dynamsql + '','''''' + @UserName + ''''''''
                           SELECT @Dynamsql = @Dynamsql + '' from ##MagInserted i full outer join ##MagDeleted d''
                           SELECT @Dynamsql = @Dynamsql + @KeyColumn
                           SELECT @Dynamsql = @Dynamsql + '' where i.'' + @columnname + '' <> d.'' + @columnname
                           SELECT @Dynamsql = @Dynamsql + '' or (i.'' + @columnname + '' is null and  d.'' + @columnname + '' is not null)''
                           SELECT @Dynamsql = @Dynamsql + '' or (i.'' + @columnname + '' is not null and  d.'' + @columnname + '' is null)''
                           EXEC (@Dynamsql)
                           END    END   '
SELECT @Dynamsql
EXEC(@Dynamsql)

SET @Counter=@Counter-1

END

所以这是测试用例和输出:

enter image description here

答案 2 :(得分:2)

加入插入,选中和检查列。注意NULL安全非相等性比较,将非NULL值设置为NULL,反之亦然,将其视为已更改的数据。

CREATE TRIGGER [dbo].[T1_TraceUpdate]
    on [dbo].[T1]
    AFTER update
AS 
BEGIN
    set nocount on;
    -- Changing Id breaks the logic of the trigger
    IF UPDATE(ID) 
    BEGIN 
        RAISERROR ('ID cannot change.', 16, 1);
        --
    END     
    INSERT INTO dbo.History(ObjectId, [Column], OldValue, NewValue)
    SELECT inserted.ID, ColumnName, OldValue, NewValue
    FROM inserted 
    INNER JOIN Deleted ON  Deleted.Id = Inserted.Id
    CROSS APPLY
       (SELECT ColumnName='Title', OldValue=deleted.Title, NewValue=inserted.Title 
          WHERE isnull(nullif(inserted.Title,deleted.Title), nullif(deleted.Title,inserted.Title)) IS NOT NULL 
        UNION ALL
        SELECT 'Amount', CAST(deleted.Amount AS nvarchar(max)), CAST(inserted.Amount AS nvarchar(max))
          WHERE isnull(nullif(inserted.Amount,deleted.Amount), nullif(deleted.Amount,inserted.Amount)) IS NOT NULL  
        UNION ALL
        SELECT 'Price', CAST(deleted.Price AS nvarchar(max)), CAST(inserted.Price AS nvarchar(max))
          WHERE isnull(nullif(inserted.Price, deleted.Price), nullif(deleted.Price,inserted.Price)) IS NOT NULL
        UNION ALL
        -- you may want to skip version (`timestamp` type) as it's always updated by server
        SELECT 'Version', CONVERT(varchar(max),CONVERT(VARBINARY,deleted.Version),1), CONVERT(varchar(max),CONVERT(VARBINARY,inserted.Version),1)
          WHERE isnull(nullif(inserted.Version, deleted.Version), nullif(deleted.Version,inserted.Version)) IS NOT NULL
     ) t;
END;  

答案 3 :(得分:1)

我无法为您提供我们的专有解决方案,但我可以建议一种方法。我们所做的是我们有一个脚本,它在系统表中查找指定表的列,然后使用动态SQl生成触发器代码,并使用循环一次抓取所有列。

每次更改表结构或创建要审核的新表时,我们都会运行此脚本。因此实际的触发器看起来与发布的其他解决方案有些类似,但它是通过一个脚本创建的,该脚本使用动态SQL遍历所有列并将它们添加到脚本中以创建触发器。

这比错误和错失更容易,而不是手动生成每个触发器。这种方法在我们的遗留数据库上运行了15年以上。为了使事情更容易理解,我们有一个插入触发器,一个用于删除,一个用于每个表的更新。

答案 4 :(得分:1)

您也可以使用CDC(更改数据捕获)

  1. 为CDC启用数据库
  2. 启用CDC的表/列
  3. 读取CDC表以读取更改(每个实际更改都有旧行和新行,+ $ updatemask,您可以在其中找到更新的列。它会自动排除虚假更新)
  4. 所有这些东西都可以为每个表动态完成。

    触发器会降低OLTP系统的速度。在触发器完成之前,不会提交事务。 CDC在事务提交后和您需要时工作 - CDC扫描程序直接读取事务日志,就像在事务复制中一样。 CDC的优点是您可以读取CDC表,而不是基表。您可以在安静时间延迟该过程并读取/填充历史表,例如在周末或过夜。