87

我正在执行UPDATEwithOUTPUT查询:

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

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

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

现在 SQL Server 团队在一篇博文中解释了这个问题——带有 OUTPUT 子句的 UPDATE——触发器——和 SQLMoreResults

错误消息是不言自明的

他们还给出了解决方案:

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

除了我无法对整个博客文章做出判断。

所以让我问我的问题:我应该改变什么UPDATE才能使它起作用?

也可以看看

4

3 回答 3

63

能见度警告:不要其他答案。它会给出不正确的值。继续阅读为什么它是错误的。


UPDATE鉴于在 SQL Server 2008 R2中工作所需的复杂性OUTPUT,我将查询更改为:

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 本身也使用了同样的 hack!

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


更新:使用 OUTPUT 是有害的

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

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

然后达到了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

除了因为不允许将 atimestamp插入表(甚至是临时表变量)而失败。

解决方法尝试 #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)
    • OUTPUT 检索新的时间戳(即 3)
    • 触发运行
      • 再次修改行
        • 时间戳已更新(例如 3 → 4)
  • UPDATE 语句完成
  • OUTPUT 返回3 (错误值)

这表示:

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

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

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

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

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

Entity Framework 是如何解决的?

.NET Entity Framework 使用 rowversion 实现乐观并发。EF 依赖于在timestamp发布 UPDATE 后知道它存在的值。

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

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

为了检索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

不要使用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 子句。

于 2012-11-02T16:04:06.790 回答
54

要解决此限制,您需要做 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那么您可能不会发现结果有用,但这只是触发器的一个子集。上述技术在其他情况下可以正常工作,例如触发器记录到其他表以进行审计,或者即使原始行被写回触发器中也返回插入的标识值。

于 2012-11-02T15:43:06.207 回答
3

为什么将所有需要的列放入表变量中?我们只需要主键,我们可以在 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;
于 2018-08-13T09:14:41.380 回答