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

Ian*_*oyd 59 sql sql-server sql-server-2008-r2

我执行UPDATEOUTPUT查询:

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid
Run Code Online (Sandbox Code Playgroud)

这句话很好,很好; 直到在表上定义了一个触发器.然后我的UPDATE陈述将得到错误334:

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

现在,SQL Server团队博客文章中解释了这个问题:

错误消息是不言自明的

他们还提供解决方案:

该应用程序已更改为使用INTO子句

除了我无法做出整篇博文的正面或反面.

所以让我问一下我的问题:我应该改变什么UPDATE才能使它有效?

也可以看看

Mar*_*ith 45

要解决这个限制,你需要 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 
Run Code Online (Sandbox Code Playgroud)

As cautioned in the other answer if your trigger writes back to the rows modified by the UPDATE statement itself in such a way that it affects the columns that you are OUTPUT-ing then you may not find the results useful but this is only a subset of triggers. The above technique works fine in other cases, such as triggers recording to other tables for audit purposes, or returning inserted identity values even if the original row is written back to in the trigger.

  • @IanBoyd只包含一个省略时间戳列的列列表.`INTO @T(Foo,bar,Baz)`或者如果你实际上试图填充它,则在表变量中声明它为`binary(8)`而不是`timestamp`. (4认同)
  • 当`@T`表包含`timestamp`列时,此解决方法失败 - 例如,当您需要返回行的新时间戳以进行乐观锁定时.SQL Server不允许您指定`timestamp`列的值,即使该列位于临时表变量中也是如此. (2认同)
  • @IanBoyd - 我已回滚您的编辑。如果您想编辑,请在其他场所进行。比如你自己的回答。 (2认同)

Ian*_*oyd 35

可见性警告:不要使用最高投票答案.它会给出不正确的值.继续阅读它的错误方式.


考虑到在SQL Server 2008 R2中UPDATE使用OUTPUT工作所需的kludge ,我改变了我的查询:

UPDATE BatchReports  
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid
Run Code Online (Sandbox Code Playgroud)

至:

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

UPDATE BatchReports
SET IsProcessed = 1
WHERE BatchReports.BatchReportGUID = @someGuid
Run Code Online (Sandbox Code Playgroud)

基本上我停止使用OUTPUT.这并不是因为实体框架本身使用这个相同的黑客!

希望2012 2014 2016 2018将有更好的实施.


更新:使用OUTPUT是有害的

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

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid
Run Code Online (Sandbox Code Playgroud)

然后在SQL Server中遇到众所周知的限制(不会修复错误):

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

解决方法尝试#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
Run Code Online (Sandbox Code Playgroud)

除非失败,因为您不允许timestamp在表中插入一个(即使是临时表变量).

解决方法尝试#2

我们秘密地知道a timestamp实际上是一个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
Run Code Online (Sandbox Code Playgroud)

这是有效的,除了价值是错误的.

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

  • 返回时间戳:0x0000000001B71692
  • 实际时间戳:0x0000000001B71693

这是因为OUTPUT我们表中的值不是 UPDATE语句末尾的值:

  • UPDATE语句开始
    • 修改行
    • 时间戳已更新
    • 检索新的时间戳
    • 触发器运行
      • 修改行
      • 时间戳已更新
  • UPDATE语句完成

这意味着:

  • 我们没有得到UPDATE语句末尾存在的时间戳
  • 我们得到UPDATE语句中不确定中间的时间戳
  • 我们没有得到正确的时间戳

任何修改行中任何值的触发器都是如此.OUTPUT将不会在UPDATE结束时输出该值.

这意味着您不信任OUTPUT返回任何正确的值

这个痛苦的现实记录在BOL中:

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

实体框架是如何解决的?

.NET实体框架使用rowversion进行乐观并发.EF取决于在timestamp发出UPDATE后知道它的值.

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

解决方法#3 - 最终

为了检索after值,实体框架问题:

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

SELECT [RowVersion], [LastModifiedDate]
FROM [dbo].[BatchReports]
WHERE @@ROWCOUNT > 0 AND [BatchReportGUID] = @1
Run Code Online (Sandbox Code Playgroud)

不要用OUTPUT.

是的,它有竞争条件,但这是最好的SQL Server可以做到的.

  • 大家好,我们不得不求助于这样的事情多少钱?我正在研究350行存储过程,出于测试目的,我打算输入OUTPUT语句.当然我不能因为这个bug.愚蠢的疏忽微软. (13认同)
  • 2016年没有更好。 (3认同)
  • 您可以通过将`UPDATE`和`SELECT`放入事务中,并在UPDATE中放入适当的隔离级别或查询提示来解决竞争条件,但代价是并发性略有降低。 (3认同)
  • SQL Server 2014仍然提供相同的错误消息 (2认同)
  • 如果您可以切换查询,那就很好了,但是如果您像我一样必须使用3D派对DAL库,而该库使用的是`OUTPUT`而不是`INTO`,那么您只能禁用/重新启用或完全删除触发器。 (2认同)
  • @Ryan 在答案中添加了“INSERT”的答案。这就是实体框架的作用;他们也放弃了使用“OUTPUT”来输出任何内容。 (2认同)