SQL Server BEFORE UPDATE触发器,它在UPDATE执行之前在字段上添加时间戳

时间:2013-11-14 18:37:14

标签: sql sql-server sql-server-2008 tsql

好的stackoverflow人员,

我正在研究用于审计目的的SQL SERVER 2008 R2上的表的触发器,它应该在UPDATE查询发送执行之前添加UPDATE_TS字段的时间戳。结果是更新发生时,要在查询时更新的原始值加上触发器设置的UPDATE_TS的附加值。

我已经编辑了这个问题,因为我听说内部联接在触发器的性能方面与不使用它们相比并不是很重。我不确定这是否会在触发器上增加额外的开销,而不是避免在触发器中进行内部连接。

我正在研究的例子如下。感谢您的任何帮助和建议!

示例表名为MY_TABLE:

CREATE TABLE [myschema].[MY_TABLE](
[MY_TABLE_ID] [bigint] IDENTITY(1,1) NOT NULL,
[FIELD_TO_UPDATE] [varchar](255) NOT NULL,
[CREATE_TS] [datetime] NULL,
[UPDATE_TS] [datetime] NULL),
PRIMARY KEY (MY_TABLE_ID))

TRIGGER创建:

CREATE TRIGGER [myschema].[my_table_update_ts_trigger] ON [mydb].[myschema].[MY_TABLE]
INSTEAD OF UPDATE
AS
BEGIN
    UPDATE INTO MY_TABLE ([FIELD_TO_UPDATE],[UPDATE_TS])
    SELECT ins.FIELD_TO_UPDATE, GETDATE() FROM INSERTED as ins
END

2 个答案:

答案 0 :(得分:11)

您需要标识需要更新的行,并使用连接或半连接执行此操作。除非你根本不执行更新,否则它不会比这更有效:

CREATE TRIGGER [myschema].[my_table_update_ts_trigger] 
ON [myschema].[MY_TABLE]
INSTEAD OF UPDATE
AS
BEGIN
    UPDATE t SET 
      FIELD_TO_UPDATE = i.FIELD_TO_UPDATE,
      UPDATE_TS = CURRENT_TIMESTAMP
    FROM myschema.MY_TABLE AS t
    INNER JOIN inserted AS i
    ON t.MY_TABLE_ID = i.MY_TABLE_ID;
END
GO

这是执行计划:

Execution plan for instead of UPDATE trigger using INNER JOIN

因为您需要将inserted中的行与基表匹配,并且因为可能有多行被任何操作更新(在SQL中触发fire per statement 服务器,而不是每行,就像在其他一些平台上一样),并且因为这不是BEFORE更新,而是INSTEAD OF更新(意味着您仍然必须实际执行在没有触发器的情况下发生的UPDATE在适当的位置),您需要从两个表中输出以准确执行更新。这意味着您需要一个JOIN,并且您不能使用SEMI-JOIN(例如EXISTS),这可能仍然违反了您的古怪要求。如果您只需要更新时间戳,则可以执行以下操作:

UPDATE t SET UPDATE_TS = CURRENT_TIMESTAMP
  FROM myschema.MY_TABLE AS t
  WHERE EXISTS (SELECT 1 FROM inserted WHERE MY_TABLE_ID = t.MY_TABLE_ID);

不幸的是,这不起作用,因为FIELD_TO_UPDATE在没有实际拉入正确连接中的inserted伪表的情况下丢失了。

另一种方法是使用CROSS APPLY,例如:

 UPDATE t SET 
   FIELD_TO_UPDATE = i.FIELD_TO_UPDATE,
   UPDATE_TS = CURRENT_TIMESTAMP
FROM  inserted AS i
CROSS APPLY myschema.MY_TABLE AS t
WHERE i.MY_TABLE_ID = t.MY_TABLE_ID;

它也缺少令人讨厌的JOIN关键字,但它仍在执行JOIN。您可以看到这一点,因为执行计划是相同的:

Execution plan for instead of UPDATE trigger using CROSS APPLY

现在,理论上你可以在没有连接的情况下做到这一点,但这并不意味着它会表现得更好。事实上,我保证你不会怀疑这样效率会降低,即使它不包含像JOIN这样的单个四个字母的单词:

    DECLARE @NOW             DATETIME = CURRENT_TIMESTAMP,
            @MY_TABLE_ID     INT,
            @FIELD_TO_UPDATE VARCHAR(255);

    DECLARE c CURSOR LOCAL FAST_FORWARD FOR
      SELECT MY_TABLE_ID, FIELD_TO_UPDATE FROM inserted;

    OPEN c;

    FETCH NEXT FROM c INTO @FIELD_TO_UPDATE, @MY_TABLE_ID;

    WHILE @@FETCH_STATUS = 0
    BEGIN
      UPDATE myschema.MY_TABLE SET 
          FIELD_TO_UPDATE = @FIELD_TO_UPDATE,
                UPDATE_TS = @NOW
        WHERE MY_TABLE_ID = @MY_TABLE_ID;

      FETCH NEXT FROM c INTO @FIELD_TO_UPDATE, @MY_TABLE_ID;
    END

    CLOSE c;
    DEALLOCATE c;

那就是说,如果你认为这个解决方案会比连接的解决方案更快,我会在佛罗里达州有一些沼泽地卖给你。酒店也有多座桥梁。我甚至都不打算为这个显示执行计划。

让我们比较INSTEAD OF INSERT触发器中发生的事情。这是一个例子,可能类似于你所拥有的:

CREATE TRIGGER myschema.ins_my_table
ON myschema.MY_TABLE
INSTEAD OF INSERT
AS
  INSERT myschema.MY_TABLE(FIELD_TO_UPDATE, CREATE_TS)
    SELECT FIELD_TO_UPDATE, CURRENT_TIMESTAMP FROM inserted;
GO

这也会产生一个看似执行了两个查询的计划:

Execution plan for instead of INSERT trigger with no JOIN

重要的是要注意INSTEAD OF触发器会取消原始更新,并且您负责发布自己的更新(即使该计划仍显示两个查询)。

最后一个选项是使用AFTER触发器而不是INSTEAD OF触发器。这将允许您在没有JOIN的情况下更新时间戳,因为FIELD_TO_UPDATE已经更新。但在这种情况下,你真的会看到两个查询,并且实际上会执行两个查询(它不会在计划中看起来那样)。

一些一般性意见

  

由于我要提高性能,我不想在用于触发器的代码中使用任何内连接。

这没有多大意义;为什么你认为连接对性能有害?听起来你看过太多的NoSQL视频。请不要丢弃技术,因为你听说它很糟糕,或者因为你的连接速度很慢。创建有意义的查询,在效果不佳时进行优化,并在无法优化时寻求帮助。几乎在所有情况下(当然都有例外),问题是索引或统计信息,而不是您使用JOIN关键字的事实。这并不意味着您应该不惜一切代价避免所有查询中的所有联接。

答案 1 :(得分:0)

如果只不想看到JOIN一词,那就很有可能,就这样写吧。

CREATE TRIGGER [myschema].[my_table_update_ts_trigger] 
ON [myschema].[MY_TABLE]
INSTEAD OF UPDATE
AS
BEGIN
    UPDATE t
    SET FIELD_TO_UPDATE = i.FIELD_TO_UPDATE,
        UPDATE_TS       = CURRENT_TIMESTAMP
    FROM myschema.MY_TABLE AS t, 
         inserted AS i
    WHERE t.MY_TABLE_ID = i.MY_TABLE_ID;
END
GO