由于 INSTEAD OF 触发器,OUTPUT 子句为新插入的标识值返回 0

Han*_*non 8 trigger sql-server output-clause

考虑以下最小、完整且可验证的示例代码(请参阅此处的 dbfiddle):

CREATE TABLE [dbo].[test]
(
      [i] bigint NOT NULL 
        identity(1,1) 
        PRIMARY KEY CLUSTERED
    , [d] varchar(10) NOT NULL
);
GO
Run Code Online (Sandbox Code Playgroud)

使用INSTEAD OF INSERT, UPDATE触发器:

CREATE TRIGGER [dbo_test_trigger]
ON [dbo].[test]
INSTEAD OF INSERT, UPDATE
AS
BEGIN
    IF ROWCOUNT_BIG() = 0 RETURN;

    SET NOCOUNT ON;

    MERGE INTO [dbo].[test] [target]
    USING [inserted] [source] ON [target].[i] = [source].[i]
    WHEN NOT MATCHED THEN
        INSERT
        (
            [d]
        )
        VALUES 
        (
            [source].[d]
        )
    WHEN MATCHED THEN 
        UPDATE
        SET [target].[d] = [source].[d];
END;
GO
Run Code Online (Sandbox Code Playgroud)

我正在对表进行插入,希望获得插入的标识值,但是返回的值是0

DECLARE @output TABLE
(
      [i] bigint NOT NULL
    , [d] varchar(10) NOT NULL
);

INSERT INTO [dbo].[test]
(
    [d]
)
OUTPUT 
      [inserted].[i]
    , [inserted].[d]
INTO @output 
(
      [i]
    , [d]
) 
VALUES ('test');

/* shows [i] is 0 */
SELECT *
FROM @output;

/* shows [i] is 1 */
SELECT *
FROM [dbo].[test];
Run Code Online (Sandbox Code Playgroud)

结果是:

d
0 测试

d
1 测试

期望的结果是两组输出匹配,但事实并非如此。

我究竟做错了什么?


我已经看到了这一点,但是这似乎很不同,因为我根本没有使用视图。在我的示例中,触发器位于桌子上。

Pau*_*ite 11

这种行为令人困惑并且记录很少。

\n

它曾经在INSTEAD OF INSERT Triggers中有更好的记录(链接到 2008 R2 文档):

\n

在此输入图像描述

\n

解释是,在触发器触发之前\xe2\x80\x94 捕获值时\xe2\x80\x94 SQL Server 无法知道触发器实际将插入多少行。它不会提前分配标识值,并在已知触发器结果时尝试以某种方式将它们匹配。

\n

报价单:

\n
\n

返回的结果就像实际发生了 INSERT、UPDATE 或 DELETE 一样生成

\n
\n

传达一般概念。这并不意味着返回的结果在所有方面都与发生基础操作时完全相同。这在所有情况下都是不可能的。

\n

上一段说(强调):

\n
\n

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

\n
\n

在操作发生之前不会分配任何标识值INSTEAD OF,因此它们不能出现在OUTPUT集合中。

\n
\n

记住初始操作(本示例中的 INSERT)是针对隐藏的临时表进行的,尽管在执行计划中被标记为目标表本身,这可能会有所帮助。有关 INSTEAD OF 触发器的更多信息,请参阅我的文章“有趣的事情” 。

\n
\n

INSTEAD OF 触发器有所不同,因为这种类型的 DML 触发器完全取代了触发的操作。插入和删除的伪表现在代表了如果触发语句实际执行的话将会发生的更改。行版本控制不能用于这些触发器,因为根据定义,没有发生任何修改。那么,如果不使用行版本,SQL Server 是如何做到的呢?

\n

答案是,当 INSTEAD OF 触发器存在时,SQL Server 会修改触发 DML 语句的执行计划。执行计划不是直接修改受影响的表,而是将有关更改的信息写入隐藏的工作表。该工作表包含执行原始更改所需的所有数据、对每行执行的修改类型(删除或插入)以及 OUTPUT 子句的触发器中所需的任何信息。

\n
\n


Zik*_*ato 5

它的行为正如记录的那样。

相关引用:OUTPUT 子句 (Transact-SQL)

对于 INSTEAD OF 触发器,即使触发器操作的结果没有发生任何修改,也会生成返回的结果,就像实际发生了 INSERT、UPDATE 或 DELETE 一样。

更新的演示:

DROP TABLE IF EXISTS dbo.Test
CREATE TABLE [dbo].[test]
(
      [i] bigint NOT NULL 
        identity(1,1) 
        PRIMARY KEY CLUSTERED
    , [d] varchar(10) NOT NULL
);
GO

INSERT INTO [dbo].[test]
(
    [d]
)
VALUES ('seed');

go
CREATE TRIGGER [dbo_test_trigger]
ON [dbo].[test]
INSTEAD OF INSERT, UPDATE
AS
BEGIN
    IF ROWCOUNT_BIG() = 0 RETURN;

    SET NOCOUNT ON;

    INSERT INTO [dbo].[test] 
    OUTPUT Inserted.* , 'trigger', IDENT_CURRENT('dbo.test') INTO #output
    SELECT d FROM Inserted
    --WHERE 1 = 0

END;
GO

DROP TABLE IF EXISTS #output
CREATE table #output
(
    SeqNo int IDENTITY (1,1)
    ,  [i] bigint NOT NULL
    , [d] varchar(10) NOT NULL
    , type varchar(20)
    , identCurrent bigint
);

INSERT INTO [dbo].[test]
(
    [d]
)
OUTPUT 
      [inserted].[i]
    , [inserted].[d]
    , 'outer'
    , IDENT_CURRENT('dbo.test')
INTO #output 
VALUES ('test');

SELECT *
FROM #output;

SELECT *, IDENT_CURRENT('dbo.test') AS identCurrent
FROM [dbo].[test]
Run Code Online (Sandbox Code Playgroud)

结果 在此输入图像描述

由于临时表在嵌套作用域中可见,因此我可以看到第一个输出是外部输出(甚至在调用触发器之前)。

如果取消注释,WHERE 1 = 0您可以看到无论如何都插入了预期的值。