SQL Server历史logging表 – 通过SP或触发器填充?

在我的应用程序的SQL Server后端中,我想为一堆我的关键表创build历史表,这些关键表将追踪行更改的历史logging。

我的整个应用程序使用存储过程,没有embedded式SQL。 数据库修改这些表的唯一连接将通过应用程序和SP接口。 传统上,我一直在使用触发器来完成这个任务。

如果我在“存储过程”和“触发器”之间做出select,哪个更好? 哪个更快?

触发。

我们编写了一个GUI(内部称为Red Matrix Reloaded ),以便轻松创build/pipe理审计日志logging触发器。

这是一些使用的DDL的东西:


审计日志表

CREATE TABLE [AuditLog] ( [AuditLogID] [int] IDENTITY (1, 1) NOT NULL , [ChangeDate] [datetime] NOT NULL CONSTRAINT [DF_AuditLog_ChangeDate] DEFAULT (getdate()), [RowGUID] [uniqueidentifier] NOT NULL , [ChangeType] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [TableName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [FieldName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [OldValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [NewValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [Username] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [Hostname] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [AppName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [UserGUID] [uniqueidentifier] NULL , [TagGUID] [uniqueidentifier] NULL , [Tag] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ) 

触发logging插入

 CREATE TRIGGER LogInsert_Nodes ON dbo.Nodes FOR INSERT AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /*We dont' log individual field changes Old/New because the row is new. So we only have one record - INSERTED*/ INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'INSERTED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName '', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag null, --OldValue null --NewValue FROM Inserted i 

触发logging更新

 CREATE TRIGGER LogUpdate_Nodes ON dbo.Nodes FOR UPDATE AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /* ParentNodeGUID uniqueidentifier */ IF UPDATE (ParentNodeGUID) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'ParentNodeGUID', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag d.ParentNodeGUID, --OldValue i.ParentNodeGUID --NewValue FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.ParentNodeGUID IS NULL AND i.ParentNodeGUID IS NOT NULL) OR (d.ParentNodeGUID IS NOT NULL AND i.ParentNodeGUID IS NULL) OR (d.ParentNodeGUID <> i.ParentNodeGUID) END /* Caption varchar(255) */ IF UPDATE (Caption) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'Caption', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag d.Caption, --OldValue i.Caption --NewValue FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.Caption IS NULL AND i.Caption IS NOT NULL) OR (d.Caption IS NOT NULL AND i.Caption IS NULL) OR (d.Caption <> i.Caption) END ... /* ImageGUID uniqueidentifier */ IF UPDATE (ImageGUID) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'ImageGUID', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag (SELECT Caption FROM Nodes WHERE NodeGUID = d.ImageGUID), --OldValue (SELECT Caption FROM Nodes WHERE NodeGUID = i.ImageGUID) --New Value FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.ImageGUID IS NULL AND i.ImageGUID IS NOT NULL) OR (d.ImageGUID IS NOT NULL AND i.ImageGUID IS NULL) OR (d.ImageGUID <> i.ImageGUID) END 

触发logging删除

 CREATE TRIGGER LogDelete_Nodes ON dbo.Nodes FOR DELETE AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /*We dont' log individual field changes Old/New because the row is new. So we only have one record - DELETED*/ INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue,NewValue) SELECT getdate(), --ChangeDate d.NodeGUID, --RowGUID 'DELETED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName '', --FieldName d.ParentNodeGUID, --TagGUID d.Caption, --Tag null, --OldValue null --NewValue FROM Deleted d 

为了知道软件中的哪个用户进行了更新,每个连接都通过调用存储过程“自行login到SQL Server”:

 CREATE PROCEDURE dbo.SaveContextUserGUID @UserGUID uniqueidentifier AS /* Saves the given UserGUID as the session's "Context Information" */ IF @UserGUID IS NULL BEGIN PRINT 'Emptying CONTEXT_INFO because of null @UserGUID' DECLARE @BinVar varbinary(128) SET @BinVar = CAST( REPLICATE( 0x00, 128 ) AS varbinary(128) ) SET CONTEXT_INFO @BinVar RETURN 0 END DECLARE @UserGUIDBinary binary(16) --a guid is 16 bytes SELECT @UserGUIDBinary = CAST(@UserGUID as binary(16)) SET CONTEXT_INFO @UserGUIDBinary /* To load the guid back DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID select @SavedUserGUID AS UserGUID */ 

笔记

  • Stackoverflow代码格式删除大部分空白行 – 所以格式化太糟糕了
  • 我们使用用户表,而不是集成安全性
  • 这个代码是作为一个易于使用的提供 – 我们的deviseselect允许没有批评。 纯粹主义者可能会坚持认为所有的日志代码都应该在业务层面上完成 – 他们可以来这里为我们写信/维护它。
  • 斑点不能使用SQL Server中的触发器logging(没有“之前”版本的斑点 – 只有什么)。 文本和nText是斑点 – 这使笔记无法login,或使他们varchar(2000)的。
  • 标签列被用作任意文本来标识行(例如,如果客户被删除,标签将在审计日志表中显示“通用汽车北美公司”。
  • TagGUID用来指向行的“父”。 例如,日志loggingInvoiceLineItems指向InvoiceHeader 。 通过这种方式,任何search与特定发票相关的审计日志条目的人都将通过审计追踪中的订单项TagGUIDfind已删除的“订单项”。
  • 有时“OldValue”和“NewValue”值被写为子select – 以获得有意义的string。 即”

    OldValue:{233d-ad34234 ..} NewValue:{883-sdf34 …}

在审计线索中的用处不大:

 OldValue: Daimler Chrysler NewValue: Cerberus Capital Management 

最后说明 :请随意不要做我们做的事情。 这对我们很好,但其他人都可以自由使用。

在SQL Server 2008中,MSDN上称为CDC(更改数据捕获) CDC的新function可以提供帮助。 CDC是一种将表数据更改logging到另一个表中而不用编写触发器或其他机制的function。更改数据捕获将logging插入,更新和删除等更改logging到SQL Server中的表中,从而使关系中的更改的详细信息格式。

Channel9video

我们有一个我们用来生成触发器的第三方工具ApexSQL Audit 。

以下是触发器如何在后台显示以及如何存储数据。 希望大家会发现这个function足以对这个过程进行逆向工程。 它与Ian Boyd在他的例子中显示的有些不同,因为它允许每个栏目分别被审计。

表1 – 保存交易细节(谁,何时,申请,主机名等)

 CREATE TABLE [dbo].[AUDIT_LOG_TRANSACTIONS]( [AUDIT_LOG_TRANSACTION_ID] [int] IDENTITY(1,1) NOT NULL, [DATABASE] [nvarchar](128) NOT NULL, [TABLE_NAME] [nvarchar](261) NOT NULL, [TABLE_SCHEMA] [nvarchar](261) NOT NULL, [AUDIT_ACTION_ID] [tinyint] NOT NULL, [HOST_NAME] [varchar](128) NOT NULL, [APP_NAME] [varchar](128) NOT NULL, [MODIFIED_BY] [varchar](128) NOT NULL, [MODIFIED_DATE] [datetime] NOT NULL, [AFFECTED_ROWS] [int] NOT NULL, [SYSOBJ_ID] AS (object_id([TABLE_NAME])), PRIMARY KEY CLUSTERED ( [AUDIT_LOG_TRANSACTION_ID] ASC ) ) 

表2 – 在值之前/之后。

 CREATE TABLE [dbo].[AUDIT_LOG_DATA]( [AUDIT_LOG_DATA_ID] [int] IDENTITY(1,1) NOT NULL, [AUDIT_LOG_TRANSACTION_ID] [int] NOT NULL, [PRIMARY_KEY_DATA] [nvarchar](1500) NOT NULL, [COL_NAME] [nvarchar](128) NOT NULL, [OLD_VALUE_LONG] [ntext] NULL, [NEW_VALUE_LONG] [ntext] NULL, [NEW_VALUE_BLOB] [image] NULL, [NEW_VALUE] AS (isnull(CONVERT([varchar](8000), [NEW_VALUE_LONG],0),CONVERT([varchar](8000),CONVERT([varbinary](8000),substring([NEW_VALUE_BLOB],(1),(8000)),0),0))), [OLD_VALUE] AS (CONVERT([varchar](8000),[OLD_VALUE_LONG],0)), [PRIMARY_KEY] AS ([PRIMARY_KEY_DATA]), [DATA_TYPE] [char](1) NOT NULL, [KEY1] [nvarchar](500) NULL, [KEY2] [nvarchar](500) NULL, [KEY3] [nvarchar](500) NULL, [KEY4] [nvarchar](500) NULL, PRIMARY KEY CLUSTERED ( [AUDIT_LOG_DATA_ID] ASC ) ) 

插入触发器

我没有显示更新的触发器,因为它们相当长,并且具有与此相同的逻辑。

 CREATE TRIGGER [dbo].[tr_i_AUDIT_Audited_Table] ON [dbo].[Audited_Table] FOR INSERT NOT FOR REPLICATION As BEGIN DECLARE @IDENTITY_SAVE varchar(50), @AUDIT_LOG_TRANSACTION_ID Int, @PRIM_KEY nvarchar(4000), @ROWS_COUNT int SET NOCOUNT ON Select @ROWS_COUNT=count(*) from inserted Set @IDENTITY_SAVE = CAST(IsNull(@@IDENTITY,1) AS varchar(50)) INSERT INTO dbo.AUDIT_LOG_TRANSACTIONS ( TABLE_NAME, TABLE_SCHEMA, AUDIT_ACTION_ID, HOST_NAME, APP_NAME, MODIFIED_BY, MODIFIED_DATE, AFFECTED_ROWS, [DATABASE] ) values( 'Audited_Table', 'dbo', 2, -- ACTION ID For INSERT CASE WHEN LEN(HOST_NAME()) < 1 THEN ' ' ELSE HOST_NAME() END, CASE WHEN LEN(APP_NAME()) < 1 THEN ' ' ELSE APP_NAME() END, SUSER_SNAME(), GETDATE(), @ROWS_COUNT, 'Database_Name' ) Set @AUDIT_LOG_TRANSACTION_ID = SCOPE_IDENTITY() --This INSERT INTO code is repeated for each columns that is audited. --Below are examples for only two columns INSERT INTO dbo.AUDIT_LOG_DATA ( AUDIT_LOG_TRANSACTION_ID, PRIMARY_KEY_DATA, COL_NAME, NEW_VALUE_LONG, DATA_TYPE , KEY1 ) SELECT @AUDIT_LOG_TRANSACTION_ID, convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')), 'Column1', CONVERT(nvarchar(4000), NEW.[Column1], 0), 'A' , CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0)) FROM inserted NEW WHERE NEW.[Column1] Is Not Null --value is inserted for each column that is selected for auditin INSERT INTO dbo.AUDIT_LOG_DATA ( AUDIT_LOG_TRANSACTION_ID, PRIMARY_KEY_DATA, COL_NAME, NEW_VALUE_LONG, DATA_TYPE , KEY1 ) SELECT @AUDIT_LOG_TRANSACTION_ID, convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')), 'Column2', CONVERT(nvarchar(4000), NEW.[Column2], 0), 'A' , CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0)) FROM inserted NEW WHERE NEW.[Column2] Is Not Null End 

免责声明:我不以任何方式隶属于Apex,但是我在我目前的工作中使用他们的工具。

正如其他人所说,触发器。 他们更容易进行unit testing,并且能够让用户直接访问表中的随机查询,从而使用户更加灵活。

至于更快? 确定数据库中的快速是一个大量variables的难题。 在“尝试两种方式并比较”之后,你不会得到一个有用的答案,哪种方法更快。 variables包括所涉及的表的大小,更新的正常模式,服务器中磁盘的速度,内存量,专用于caching的内存量等。这个列表是无止境的,每个variables都会影响触发器比SP内部的自定义SQL更快。

好。 快速。 低廉。 select两个。 触发器在完整性方面很好,可能在维护方面很便宜。 可以说,他们也是快速的,一旦他们工作,你就完成了他们。 SP是一个维护问题,推动维护的东西可以是快速的,但从来没有好或廉价。

祝你好运。

推荐的方法取决于您的要求。 如果历史表存在审计跟踪 ,则需要捕获每个操作。 如果历史表仅用于性能方面的原因,则预定的SQL代理数据传输作业应该足够了。

要捕获每个操作,请使用AFTER TRIGGER或更改数据捕获。

在触发器提供了两个临时表以在触发器内进行操作之后:

  • 在INSERT或UPDATE之后插入
  • 删除后删除

您可以从这些临时表执行对历史logging表的插入,并且您的历史logging表将始终保持最新状态。 您可能希望在历史logging表中添加版本编号,时间戳记或两者,以将对单个源行的更改分开。

更改数据捕获(CDC)旨在创build增量表,您可以将其用作将数据加载到数据仓库(或历史logging表)中的源。 与触发器不同,CDC是asynchronous的,您可以使用任何方法和计划来填充目标(sprocs,SSIS)。

您可以使用CDC访问原始数据和更改。 更改跟踪 (CT)仅检测更改的行。 可以用CDC而不是用CT来构build一个完整的审计跟踪。 CDC和CT都只在MSSQL 2008企业版和开发版中可用。

为此使用触发器。 这意味着,任何更改,无论来源,都会反映在历史logging表中。 这对安全性很好,对于忘记添加代码来更新历史logging表等等的故障模式也很有效。

对于这种types的操作,不可能有任何特定的速度差异,因为执行时间将由I / O占主导地位。

一个需要非常小心的问题就是确定这个表的用例,并确保它的构build正确。

具体而言,如果是针对利益相关者的操作审计跟踪,那么与表格中logging更改的前后快照完全不同。 (事实上​​,除了debugging以外,我很难想象如何更好地使用logging更改。)

审计跟踪通常至less需要一个用户ID,一个时间戳和一个操作代码 – 可能还有一些关于操作的细节。 示例 – 更改采购订单上的订单项上的订购数量。

对于这种types的审计线索,您希望使用触发器。 BR层中embedded这些事件的代数越高越好。

OTOH,对于logging级别的更改,触发器是正确的匹配。 但是从dbms日志logging文件中获取它通常也更容易。

我更喜欢使用触发器作为审计表,因为触发器可以捕获所有更新,插入和删除,而不仅仅是通过某些存储过程调用的更新,插入和删除:

 CREATE TRIGGER [dbo].[tr_Employee_rev] ON [dbo].[Employee] AFTER UPDATE, INSERT, DELETE AS BEGIN IF EXISTS(SELECT * FROM INSERTED) AND EXISTS (SELECT * FROM DELETED) BEGIN INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT inserted.ID, inserted.Firstname,inserted.Initial,inserted.Surname,inserted.Birthdate,'u', GetDate(), SYSTEM_USER FROM INSERTED END IF EXISTS (SELECT * FROM INSERTED) AND NOT EXISTS(SELECT * FROM DELETED) BEGIN INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT inserted.ID, inserted.Firstname,inserted.Initial,inserted.Surname,inserted.Birthdate,'i', GetDate(), SYSTEM_USER FROM INSERTED END IF EXISTS(SELECT * FROM DELETED) AND NOT EXISTS(SELECT * FROM INSERTED) BEGIN INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT deleted.ID, deleted.Firstname,deleted.Initial,deleted.Surname,deleted.Birthdate,'d', GetDate(), SYSTEM_USER FROM DELETED END END 

我使用SQLServer为修订表生成SQL,而不是手工编写它。 该代码可在https://github.com/newdigate/sqlserver-revision-tables中find

触发。 现在你可能会说数据更新的唯一方法是通过你的SP,但事情可能会改变,或者你可能需要做一个大规模的插入/更新,使用SP将是太麻烦了。 去触发器。

这取决于应用程序的性质和表结构,索引数量,数据大小等等,外键等等。如果这些表格是相对简单的表格(没有或像datetime / integer列上的索引那样less)具有有限的数据设置(<100万行),你可能会确定使用触发器。

请记住,触发器可能是locking问题的根源。 我会假设,如果你正在使用历史表作为一种审计线索,你将被索引它们以供将来参考。 如果触发器更新由于索引而插入/更新/删除速度慢的历史logging表,过程调用将被阻塞直到触发器结束。 另外,如果在触发器中有任何外键约束被更新,这也可能会影响性能。

在这种情况下,这一切都取决于表索引。 我们使用Sql Server 2000作为24/7应用程序,每天处理超过10万笔金融交易。 最大/主表有超过1亿行和15个索引(如果需要正常运行时间,大规模删除是不合理的)。 即使所有SQL都是在“存储过程”中完成的,但由于性能受到影响,我们不使用触发器或外键。

触发。 这是我的方法:

  1. 为每个需要审计试用的关键表创build一个审计表
  2. 审计表将包括来自源表+列审计logging信息的所有列,例如谁,何时和动作
  3. 仅触发UPDATE和DELETE,INSERT操作将在源表中拥有原始logging
  4. 在更新或删除之前,将原始logging+审计信息复制到审计表
  5. (可选 – 仅适用于更新:)要知道哪个列被更新,请使用SQL函数中内置的UPDATE(ColumnName)或COLUMNS_UPDATED()来确定受影响的列

以这种方式进行审计可以保持源表中的当前状态和审计表中的所有历史logging,并通过键列轻松识别。