【问题标题】:SQL Server history table - populate through SP or Trigger?SQL Server 历史表 - 通过 SP 或触发器填充?
【发布时间】:2008-12-08 13:15:39
【问题描述】:

在我的应用程序的 SQL Server 后端,我想为我的一组关键表创建历史表,这将跟踪行更改的历史记录。

我的整个应用程序都使用存储过程,没有嵌入式 SQL。 修改这些表的唯一数据库连接将是通过应用程序和 SP 接口。 传统上,我合作过的商店都使用触发器来执行此任务。

如果我可以在存储过程和触发器之间进行选择,哪个更好? 哪个更快?

【问题讨论】:

  • 目的是什么?如果是审计跟踪,那么触发器对于捕获谁在何时何地信息是很尴尬的,尤其是在事务中有多个更改的情况下。如果是BR相关的,in需要在BR层。谨防那些不了解或不关心您的背景的人的建议。
  • 也许是因为我很久以前就问过这个问题了。 SO 的一个主要问题是它随着时间的推移发生了变化,并且许多用户只对当前设置做出反应。 JMHO。

标签: sql-server stored-procedures triggers


【解决方案1】:

触发器。

我们编写了一个 GUI(内部称为 Red Matrix Reloaded)来轻松创建/管理审计日志触发器。

这里是一些使用的东西的 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 
)

触发记录插入

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

触发记录更新

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

触发记录删除

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

为了知道软件中的哪个用户进行了更新,每个连接都通过调用存储过程“将自己登录到 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 代码格式会删除大部分空白行 - 所以格式化很糟糕
  • 我们使用用户表,而不是集成安全性
  • 提供此代码是为了方便 - 不允许批评我们的设计选择。纯粹主义者可能会坚持所有日志记录代码都应该在业务层完成 - 他们可以来这里为我们编写/维护它。
  • 无法使用 SQL Server 中的触发器记录 blob(没有“之前”版本的 blob - 只有现有版本)。 Text 和 nText 是 blob - 这使得笔记要么无法记录,要么使它们成为 varchar(2000) 的。
  • 标签列用作标识行的任意文本(例如,如果客户被删除,标签将在审核日志表中显示“通用汽车北美公司”。
  • TagGUID 用于指向行的“父”。例如,记录 InvoiceLineItems 指向 InvoiceHeader。这样一来,任何搜索与特定发票相关的审核日志条目的人都会在审核跟踪中通过行项目的 TagGUID 找到已删除的“行项目”。
  • 有时将“OldValue”和“NewValue”值写为子选择 - 以获得有意义的字符串。即”

    旧值:{233d-ad34234..} 新值:{883-sdf34...}

在审计跟踪中的用处不如:

OldValue: Daimler Chrysler
NewValue: Cerberus Capital Management

最后说明:不要做我们所做的事。这对我们来说很好,但其他人可以随意不使用它。

【讨论】:

  • 这个例子非常有帮助。非常感谢。
  • Soooooo...如果你喜欢它,它可以成为答案吗?
  • @Ian Boyd:我似乎无法让这个解决方案发挥作用,如果可能的话,请您澄清一下触发器中使用的列。我需要实施此解决方案,但在使用的列名称无效方面遇到很多错误。
  • @SQL.NETWarrior:您应该发布您的AuditLog 表脚本,以及您的三个审核日志触发器、您尝试更新的表的脚本、您正在运行以执行的 SQL在插入/更新/删除中,您遇到新问题的错误,并从此处链接到它。
  • 虽然我认为这是一种解决方案,但我会尽量远离触发器,因为它们可能会给调试带来麻烦,而且它们不一定会根据应用程序的类型获得您想要审核的信息。如果您的 Web 应用程序是一个 sql 数据库并使用 sql 用户或集成安全性,并且您的应用程序使用成员资格提供程序,您将无法获得登录的正确用户,您将获得 USER_NAME(),这只是用户谁连接到数据库,没有应用程序逻辑被捕获。保持简单,并在应用程序的一个层中进行审计。
【解决方案2】:

在 SQL Server 2008 中,名为 CDC(更改数据捕获)CDC on MSDN 的新功能可以提供帮助。 CDC 是一种无需编写触发器或其他机制即可将表数据的更改记录到另一个表中的能力,更改数据捕获记录 SQL Server 中表的插入、更新和删除等更改,从而使更改的详细信息在关系中可用格式。

Channel9 video

【讨论】:

  • 完全像触发器,但不称为触发器。
  • 我很期待 2008 年的这一功能,但得知“变更数据捕获仅在 SQL Server 的 Enterprise、Developer 和 Evaluation 版本中可用”感到失望。
  • CDC 似乎没有记录日期、时间、登录名、主机、spid 等。我不知道,但我敢打赌没有 GUI 来管理它。而且我还假设您在启用 CBC 后无法修改表(即添加、删除、重命名列)
【解决方案3】:

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

下面是触发器在后台的外观以及数据的存储方式。希望人们会发现这足以对流程进行逆向工程。 它与 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 没有任何关系,但我确实在我目前的工作中使用了他们的工具。

【讨论】:

  • 我找到了一个非常好的通用解决方案,用于创建名为 autoaudit 的审计日志:- autoaudit.codeplex.com/downloads/get/764316 这可以满足我的所有需求,而且无需编写任何代码。
【解决方案4】:

正如其他人所说,触发器。它们更容易进行单元测试,并且对于直接访问表进行随机查询的高级用户来说更具弹性。

至于更快?确定数据库中什么是快是一个具有大量变量的难题。如果没有“尝试两种方法并进行比较”,您将不会得到关于哪种方法更快的有用答案。变量包括所涉及的表的大小、正常的更新模式、服务器中磁盘的速度、内存量、用于缓存的内存量等。这个列表是无穷无尽的,每个变量都会影响是否触发比 SP 内的自定义 SQL 更快。

很好。快速地。便宜的。选择两个。触发器在完整性方面很好,在维护方面可能很便宜。可以说他们也很快,因为一旦他们工作,你就完成了他们。 SP 是一个维护问题,将东西推入维护可能很快,但绝不是好的或便宜的。

祝你好运。

【讨论】:

    【解决方案5】:

    推荐的方法取决于您的要求。如果审计跟踪有历史记录表,则需要捕获每个操作。如果历史表只是出于性能的原因,那么计划的 SQL 代理数据传输作业应该就足够了。

    要捕获每个操作,请使用 AFTER TRIGGER 或 Change Data Capture。

    After 触发器为您提供了两个用于在触发器内部操作的临时表:

    • INSERTED 在 INSERT 或 UPDATE 之后
    • DELETED 在 DELETE 之后

    您可以从这些临时表向历史表执行插入操作,并且您的历史表将始终是最新的。您可能希望在历史记录表中添加版本编号、时间戳或同时添加两者,以分隔对单个源行的更改。

    变更数据捕获 (CDC) 旨在创建一个增量表,您可以将其用作将数据加载到数据仓库(或历史表)中的源。与触发器不同,CDC 是异步的,您可以使用任何方法和调度来填充您的目标(存储过程、SSIS)。

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

    【讨论】:

    • 我有触发器完成的历史表,但我发现了一个严重的问题。每个请求都由服务帐户下的存储过程完成,用户名存储在存储过程参数中。但是在删除触发器中我无法访问@user 参数,现在怎么办?
    【解决方案6】:

    为此使用触发器。这意味着任何更改,无论来源如何,都将反映在历史记录表中。这对安全性有好处,对故障模式有弹性,比如人们忘记添加代码来更新历史表等等。

    由于执行时间将由 I/O 控制,因此这两种操作都不太可能有任何特定的速度差异。

    【讨论】:

    • 但通常很难弄清楚是谁造成了这种变化,以及他们当时在做什么。
    • 您可以在触发器中捕获会话和登录信息并将其记录在审计表中。
    • 除非你有一个你可能不知道浏览器用户的网络应用程序,即使是在内网上
    • @gbn 完全正确,触发器对 Web 应用程序不起作用,因为身份验证和授权通常在 Web 应用程序内部处理。 USER_NAME() 对于每个更新您的应用程序的用户都将是相同的,因为该用户是允许连接到 sql server 的用户,而不是应用程序登录。
    【解决方案7】:

    需要非常小心的一个问题是确定此表的预期用例,并确保它为此目的正确构建。

    具体来说,如果是为了利益相关者的操作审计跟踪,这与表中记录更改的前后快照有很大不同。 (事实上​​,除了调试之外,我很难想象记录更改有什么用处。)

    审计跟踪通常至少需要用户 ID、时间戳和操作代码 - 可能还需要一些有关操作的详细信息。示例 - 更改采购订单中订单项的订购数量。

    对于这种类型的审计跟踪,您想使用触发器。将这些事件的生成嵌入到 BR 层中越高越好。

    OTOH,对于记录级别的更改,触发器是正确的匹配。但从您的 dbms 日志文件中获取这些信息通常也更容易。

    【讨论】:

      【解决方案8】:

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

      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 获得

      【讨论】:

        【解决方案9】:

        触发器。现在,您可能会说更新数据的唯一方法是通过您的 SP,但情况可能会发生变化,或者您可能需要进行大规模插入/更新,而使用 SP 将过于麻烦。使用触发器。

        【讨论】:

        • 但是基于行的大规模更新触发器可能会损害性能。大规模更新应包括关闭触发器、执行更新、执行第二次大规模更新以执行触发器将执行的操作,然后重新启用触发器。
        • 触发器对 Web 应用程序不起作用,因为身份验证和授权通常在 Web 应用程序内部处理。 USER_NAME() 对于每个更新您的应用程序的用户都将是相同的,因为该用户是允许连接到 sql server 的用户,而不是应用程序登录。
        【解决方案10】:

        这取决于应用程序的性质和表结构、索引数量、数据大小等、外键等。如果这些是相对简单的表(没有或很少有索引,如日期时间/整数列上的索引)有限的数据集(

        请记住,触发器可能是锁定问题的根源。我假设如果您使用历史表作为一种审计跟踪,您将对它们进行索引以供将来参考。如果触发器更新由于索引而导致插入/更新/删除缓慢的历史表,则过程调用将阻塞,直到触发器完成。此外,如果触发器中有任何要更新的外键约束,这也可能会影响性能。

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

        【讨论】:

          【解决方案11】:

          触发器。这是我的方法:

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

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

          【讨论】:

          • 触发器对 Web 应用程序不起作用,因为身份验证和授权通常在 Web 应用程序内部处理。 USER_NAME() 对于每个更新您的应用程序的用户都是相同的,因为该用户是允许连接到 sql server 的用户,而不是应用程序登录。
          • 我想知道 COLUMNS_UPDATED() 返回所有更新的列名,用逗号分隔?如果我的 3 个字段得到更新,那么 COLUMNS_UPDATED() 返回所有更新的列名,以逗号分隔?如果你知道,请告诉我。谢谢
          猜你喜欢
          • 1970-01-01
          • 2016-05-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-28
          相关资源
          最近更新 更多