当触发器在表上时,不能与OUTPUT子句一起使用UPDATE


73

我正在执行UPDATEwithOUTPUT查询:

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

这句话很好。直到在表上定义触发器为止。然后我的UPDATE语句将得到错误334

如果DML语句的目标表'BatchReports'包含不带INTO子句的OUTPUT子句,则该语句不能具有任何启用的触发器

现在,SQL Server团队在博客文章中解释了此问题-带有OUTPUT子句的UPDATE-触发器-和SQLMoreResults

错误消息是不言自明的

他们还提供解决方案:

更改了应用程序以利用INTO子句

除了我无法在整个博客文章中做文章的正面或反面。

因此,我问一个问题:我应该UPDATE将其更改为什么才能起作用?

也可以看看

Answers:


52

可见度警告:请勿回答。它将给出不正确的值。继续阅读以了解错误原因。


鉴于作出所需的杂牌UPDATEOUTPUT在SQL Server 2008 R2的工作,我改变了我的查询范围:

UPDATE BatchReports  
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

至:

SELECT BatchFileXml, ResponseFileXml, ProcessedDate FROM BatchReports
WHERE BatchReports.BatchReportGUID = @someGuid

UPDATE BatchReports
SET IsProcessed = 1
WHERE BatchReports.BatchReportGUID = @someGuid

基本上我停止使用OUTPUT。这还不错,因为Entity Framework本身也使用了这种相同的技巧!

希望2012 2014 2016 2018 2019 2019会有更好的实施。


更新:使用输出有害

我们开始的问题是尝试使用OUTPUT子句检索表中的“ after”值:

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid

然后,这达到了SQL Server中众所周知的限制(不会修复错误):

如果DML语句的目标表'BatchReports'包含不带INTO子句的OUTPUT子句,则该语句不能具有任何启用的触发器

解决方法尝试#1

因此,我们尝试使用中间TABLE变量保存OUTPUT结果的方法:

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion timestamp, 
   BatchReportID int
)
  
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

否则会失败,因为您不允许timestamp在表中插入A (即使是临时表变量也是如此)。

解决方法尝试#2

我们暗中知道atimestamp实际上是一个64位(即8字节)无符号整数。我们可以将临时表定义更改为使用binary(8)而不是timestamp

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion binary(8), 
   BatchReportID int
)
  
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

那是可行的,除了值是错误的

RowVersion我们返回的时间戳不是UPDATE完成后存在的时间戳的值:

  • 返回的时间戳0x0000000001B71692
  • 实际时间戳记0x0000000001B71693

这是因为OUTPUT表中的值不是UPDATE语句结尾处的值:

  • UPDATE语句开始
    • 修改行
      • 时间戳更新(例如2→3)
    • 输出检索新的时间戳(即3)
    • 触发运行
      • 再次修改行
        • 时间戳更新(例如3→4)
  • UPDATE语句完成
  • OUTPUT返回3 (错误的值)

这表示:

  • 我们没有获得时间戳,因为它存在于UPDATE语句的末尾(4
  • 相反,我们获得的时间戳是在UPDATE语句的不确定中间(3
  • 我们没有正确的时间戳

任何修改行中任何值的触发器都适用。该OUTPUT不会输出值作为更新的结束。

这意味着您永远无法信任OUTPUT返回任何正确的值。

BOL中记录了这种痛苦的现实:

从OUTPUT返回的列反映了INSERT,UPDATE或DELETE语句完成之后但执行触发器之前的数据。

实体框架如何解决它?

.NET实体框架使用行版本进行乐观并发。EF依赖于timestamp在发出UPDATE之后知道as的值。

由于您不能使用OUTPUT任何重要数据,因此Microsoft的Entity Framework使用与我相同的解决方法:

解决方法#3-最终-不要使用OUTPUT子句

为了检索值,实体框架发出:

UPDATE [dbo].[BatchReports]
SET [IsProcessed] = @0
WHERE (([BatchReportGUID] = @1) AND ([RowVersion] = @2))

SELECT [RowVersion], [LastModifiedDate]
FROM [dbo].[BatchReports]
WHERE @@ROWCOUNT > 0 AND [BatchReportGUID] = @1

不要使用OUTPUT

是的,它遇到了竞争问题,但这是SQL Server可以做的最好的事情。

那INSERT呢

执行实体框架的作用:

SET NOCOUNT ON;

DECLARE @generated_keys table([CustomerID] int)

INSERT Customers (FirstName, LastName)
OUTPUT inserted.[CustomerID] INTO @generated_keys
VALUES ('Steve', 'Brown')

SELECT t.[CustomerID], t.[CustomerGuid], t.[RowVersion], t.[CreatedDate]
FROM @generated_keys AS g
   INNER JOIN Customers AS t
   ON g.[CustomerGUID] = t.[CustomerGUID]
WHERE @@ROWCOUNT > 0

同样,他们使用一条SELECT语句读取该行,而不是将任何信任放在OUTPUT子句中。


17
好的,每个人,我们必须诉诸于这样的东西,这多少钱呢?我正在处理一个350行存储的proc,出于测试目的,我打算放入OUTPUT语句。我当然不能因为这个错误。傻瓜监督微软。
MAW74656 2015年

3
SQL Server 2014仍然给出相同的错误消息
Edward Olamisan '16

2
2016
Jonathan Allen

2
如果您可以切换查询,那就很好了,但是如果像我一样必须使用3D方DAL库(不使用OUTPUTW / O),INTO则只能禁用/重新启用或完全删除触发器。
2016年

3
您可以在最后一个解决方法中解决竞争条件,方法是将UPDATE和和放入SELECT事务中,并在中放入适当的隔离级别或查询提示UPDATE,以并发性稍有降低为代价。
亚历杭德罗(Alejandro)

50

要解决此限制,您需要采取 OUTPUT INTO ...一些措施。例如,从中声明一个中间表变量作为目标SELECT

DECLARE @T TABLE (
  BatchFileXml    XML,
  ResponseFileXml XML,
  ProcessedDate   DATE,
  RowVersion      BINARY(8) )

UPDATE BatchReports
SET    IsProcessed = 1
OUTPUT inserted.BatchFileXml,
       inserted.ResponseFileXml,
       deleted.ProcessedDate,
       inserted.Timestamp
INTO @T
WHERE  BatchReports.BatchReportGUID = @someGuid

SELECT *
FROM   @T 

正如在另一个答案中所警告的那样,如果您的触发器写回UPDATE语句本身修改的行,从而影响到您正在查询的列,OUTPUT那么您可能不会发现结果有用,但这只是触发器的一个子集。上面的技术在其他情况下也能很好地工作,例如出于审计目的将触发器记录到其他表中,或者即使将原始行写回到触发器中也返回插入的标识值。


2
@T表包含一timestamp列时,例如,当您需要返回行的新时间戳以进行乐观锁定时,此解决方法将失败。SQL Server不允许您为timestamp列指定值,即使该列在临时表变量中也是如此。
伊恩·博伊德

4
@IanBoyd仅包含忽略时间戳列的列列表。INTO @T (Foo, bar, Baz)或者,如果您实际上是尝试填充它,则将其声明为binary(8)table变量而不是timestamp
马丁·史密斯

1
@IanBoyd-我已回滚您的编辑。如果您想编辑,请在其他地方进行。如你自己的答案。
马丁·史密斯

2

为什么将所有需要的列放入表变量?我们只需要主键,就可以在UPDATE之后读取所有数据。使用事务时没有种族:

DECLARE @t TABLE (ID INT PRIMARY KEY);

BEGIN TRAN;

UPDATE BatchReports SET 
    IsProcessed = 1
OUTPUT inserted.ID INTO @t(ID)
WHERE BatchReports.BatchReportGUID = @someGuid;

SELECT b.* 
FROM @t t JOIN BatchReports b ON t.ID = b.ID;

COMMIT;

当然,这是另一种有效的选择。这也完全否定了OUTPUT条款的优点。您也可以将其重写为:UPDATE BatchReports SET IsProcessed=1 WHERE BatchReportGUID = @someGuid; SELECT * FROM BatchReports WHERE BatchReportGUID = @someGuid;
伊恩·博伊德

3
但是您可能没有奢侈地基于主键进行更新。OUTPUT子句仍然提供优点。
gliljas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.