1

嗨,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))

触发创建:

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
4

2 回答 2

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

这是执行计划:

使用 INNER JOIN 代替 UPDATE 触发器的执行计划

由于您需要将行匹配inserted到您的基表中,并且由于任何操作都可能更新不止一行(触发器在 SQL Server 中按语句触发,而不是像在某些其他平台中那样按行触发),并且因为这不是 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。您可以看到这一点,因为执行计划是相同的:

使用 CROSS APPLY 代替 UPDATE 触发器的执行计划

现在,理论上你可以在没有连接的情况下做到这一点,但这并不意味着它会表现得更好。事实上,我毫无疑问地向你保证,这会降低效率,即使它不包含像 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

这也会产生一个看起来像是执行了两个查询的计划:

执行计划代替没有 JOIN 的 INSERT 触发器

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

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

一些一般性评论

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

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

于 2013-11-14T18:41:21.087 回答
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
于 2018-08-23T12:49:59.550 回答