我尝试根据触发器为多个表创建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
答案 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 表格中添加了TableName
和ModifiedDate
,以便于查看。
以下是解决方案的步骤。
使用提供的信息
创建样本测试环境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
所以这是测试用例和输出:
答案 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(更改数据捕获)
所有这些东西都可以为每个表动态完成。
触发器会降低OLTP系统的速度。在触发器完成之前,不会提交事务。 CDC在事务提交后和您需要时工作 - CDC扫描程序直接读取事务日志,就像在事务复制中一样。 CDC的优点是您可以读取CDC表,而不是基表。您可以在安静时间延迟该过程并读取/填充历史表,例如在周末或过夜。