如何在 INSERT 语句中使用存储过程的 OUTPUT?

Posted

技术标签:

【中文标题】如何在 INSERT 语句中使用存储过程的 OUTPUT?【英文标题】:How can I use the OUTPUT from a stored procedure within an INSERT statement? 【发布时间】:2021-06-25 21:04:22 【问题描述】:

我正在尝试将数据从一个表复制到另一个表,并使用INSERT INTO 和存储过程将一列添加到源表中不存在的目标表中以生成附加列的值。

目标表有 counter 字段(整数),需要为插入的每一行递增(它不是 IDENTITY 列,递增此计数器由其他代码处理)。

Counter 表有多个表的计数器,需要将 Counter Name 作为参数传递给存储过程,并使用output 作为目标表的附加列。

我有一个存储过程,可以向它传递一个参数(计数器名称,在下面的示例中,计数器名称是“Counter_123”),它具有 output 值作为新的计数器值。

我可以像这样运行存储过程,它工作正常:

declare @value int
exec GetCounter 'Counter_123', @value output
PRINT @value

在这种情况下,如果计数器为 3,则 output 为 4。

这是我在INSERT INTO 中尝试做的事情:

INSERT INTO Target_Table (col1, col2, col3, uspGetCounter 'Counter_123')
SELECT (val1, val2, val3) 
FROM Source_Table
WHERE ...

返回的错误是“Counter_123”附近的语法错误

我也尝试将存储过程放在值列表中,但这也不起作用。

调用存储过程、传递参数并使用INSERT INTO 查询中的output 作为列值的语法是什么?

这里是存储过程代码供参考:

CREATE PROCEDURE [dbo].[uspGetCounter] 
    
    @CounterName varchar(30) = '', 
    @LastValue int OUTPUT
AS
BEGIN
    SET NOCOUNT ON
    
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

    BEGIN TRANSACTION

    if not exists (select * from Counter where COUNTER_NAME=@CounterName) 
       insert Counter (COUNTER_NAME, LAST_VALUE)
       values (@CounterName,0)

    select @LastValue=LAST_VALUE+1
       from Counter (holdlock)
       where COUNTER_NAME= @CounterName

    update Counter
       set LAST_VALUE=@LastValue,
           LAST_UPDATED=getdate(),
           from Counter
       where COUNTER_NAME=@CounterName

    COMMIT TRANSACTION
    
END

【问题讨论】:

如果你要使用提示,至少使用当前的语法而不是弃用的版本。现在是时候绝对准确地了解您使用的提示实际上做了什么。开始here。然后讨论这个inefficient upsert pattern。最好还是使用序列或身份。 【参考方案1】:

假设您确实需要推出自己的序列生成器,因为使用内置序列会容易得多,那么使用您当前的逻辑一次只能插入一行,因为您的存储过程没有提供任何机制一次性分配一组值。因此,您将需要某种循环,例如光标,例如

DECLARE @Counter int, @Val1 int, @Val2 int, @Val3 int;

DECLARE MyCursor CURSOR LOCAL FOR
    SELECT val1, val2, val3 
    FROM Source_Table
    WHERE ...;

WHILE 1 =1 BEGIN
    FETCH NEXT FROM MyCursor INTO @Val1, @Val2, @Val3;
    IF @@FETCH_STATUS != 0 BREAK;

    EXEC uspGetCounter 'Counter_123', @Counter OUTPUT;

    INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
        VALUES (@Val1, @Val2, @Val3, @Counter);
END;

在您需要为每一行运行 SP 时,您需要插入并获得一个新的序列值。

如果使用这种方法,我还将按照以下方式修改您的 SP 以提高性能:

-- Attempt an update and assignment in a single statement
UPDATE dbo.[Counter] SET
  @LastValue = LAST_VALUE = LAST_VALUE + 1
  , LAST_UPDATED = GETDATE()
WHERE COUNTER_NAME = @CounterName;

-- If doesn't exist (less likely case as only the first time the counter is accessed)
IF @@ROWCOUNT = 0 BEGIN
    SET @LastValue = 1;
    -- Create a new record if none exists
    INSERT INTO dbo.[Counter] (COUNTER_NAME, LAST_VALUE, LAST_UPDATED)
        SELECT @CounterName, @LastValue, GETDATE();
END;

这减少了访问并加快了 SP。


但是,再次假设您需要滚动自己的序列,如果您经常使用它来复制数据,您最好修改存储过程以一次性分配一个值块,即当您调用您的 SP 时传递一个参数@NumValuesRequired 然后你的返回值是第一个这么长的块。然后你可以一次完成你的副本,例如

DECLARE @RecordCount int, @CounterStart int;

SELECT @RecordCount = COUNT(*)
FROM Source_Table
WHERE ...;

EXEC uspGetCounter2 'Counter_123', @RecordCount, @CounterStart OUTPUT;

INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
    SELECT val1, val2, val3, @CounterStart + ROW_NUMBER() OVER () - 1
    FROM Source_Table
    WHERE ...;

【讨论】:

我希望避免使用光标,但看起来这是我唯一的选择。我无法更改计数器表或功能,它通常在用户插入新记录时由应用程序更新,因此计数器可以随时在我的程序之外增加。这被用来导入一些数据,所以我必须在每次 INSERT 时检查并增加计数器,以确保它没有被更新。 我现在。谢谢你的帮助。我已经用我需要的附加字段修改了游标示例,它运行良好。谢谢。【参考方案2】:

首先,程序放错地方了。 INSERT 语句的那部分需要列 names,而不是列 values。如果您可以以这种方式运行它(您不能),那么您对该过程所做的任何事情都将放在该语句的SELECT 子句中。

顺便说一句,现有的Counter 表和存储过程已经过时。它们的存在是为了解决一个已作为核心功能纳入 SQL Server 的问题。因此这里的 BEST 解决方案是将计数器(全部)转换为使用SEQUENCE 和NEXT VALUE FOR 语法。

然后,假设您已将序列设置为正确的值,INSERT 将如下所示:

INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT (val1, val2, val3, NEXT VALUE FOR Counter_123) 
FROM Source_Table
WHERE ...

如果由于某种原因这不是一个选项,我会将计数器逻辑滚动到查询中以在单个事务中运行,而不是在许多事务中运行,如下所示:

BEGIN TRANSACTION

DECLARE @LastValue int;
SELECT @LastValue = LAST_VALUE
       FROM Counter (holdlock)
       WHERE COUNTER_NAME = 'Counter_123'

INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT val1, val2, val3, @LastValue + ROW_NUMBER() OVER (ORDER BY val1, val2, val3) 
FROM Source_Table
WHERE ...

UPDATE Counter
SET LAST_VALUE = (SELECT MAX([Counter_123]) FROM [Target_Table])
     ,LAST_UPDATED=current_timestamp
WHERE COUNTER_NAME = 'Counter_123'

COMMIT

【讨论】:

我无法更改计数器表或功能,它通常在用户插入新记录时由应用程序更新,因此计数器可以随时在我的程序之外递增。该应用程序的代码库已接近 15 年,它没有使用任何内置的数据库功能,例如 SEQUENCE。看起来光标方法是唯一的方法,这不是用于生产的,所以不需要很快。 你看到我回答的最后一部分了吗?在该代码示例中使用 TRANSACTION 可以安全地使用 Counter 表。 是的,我看到了。毫无疑问,它会起作用,但使用这个数据库的主要应用程序是古老的、未记录的代码(用很久以前弃用的 4GL 包编写),我无权访问或修改。我使用 sproc 一次获得一个计数器增量,以最大限度地减少对该表的锁定。用户可以随时在该表中创建新记录,并且在这种情况下应用程序将如何处理该表上的锁定是未知的,我不想导致它崩溃或冒数据损坏的风险。我希望这能解释这种情况。 与请求 1000 个单独的 ID(每个 ID 有一个单独的锁)相比,在单个基于集合的事务组中执行此操作的整体锁定将减少 MUCH

以上是关于如何在 INSERT 语句中使用存储过程的 OUTPUT?的主要内容,如果未能解决你的问题,请参考以下文章

MariaDB 存储过程 - 在 INSERT 语句中出现错误“缺少 SELECT”

C# 解析 SQL 语句以查找存储过程中使用的所有 INSERT/UPDATE/DELETE 表

有一张sql表,共8列,其中6列是必须入力项,后2列是允许为空列。 在sql存储过程中写了一个insert语句

赵强老师Oracle存储过程中的out参数

赵强老师Oracle存储过程中的out参数

oracle 存储过程中 使用insert into Table (select) 进行数据批量添加 执行后无数据 但单独将上述insert