如何实现条件Upsert存储过程?

时间:2009-07-09 22:15:27

标签: sql sql-server sql-server-2005 stored-procedures upsert

我正在尝试实现您的基本UPSERT功能,但有一点麻烦:有时我不想实际更新现有行。

基本上我正在尝试在不同的存储库之间同步一些数据,并且Upsert函数似乎是要走的路。所以主要基于Sam Saffron's answer to this question,以及其他一些研究和阅读,我提出了这个存储过程:

(注意:我正在使用MS SQL Server 2005,因此MERGE语句不是一个选项)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null,
    @pTeaser varchar(255) = null 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    BEGIN TRANSACTION

        UPDATE dbo.Item WITH (SERIALIZABLE)
        SET Title = @pTitle,
            Teaser = @pTeaser
        WHERE ContentID = @pContentID

        IF @@rowcount = 0
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

    COMMIT TRANSACTION
END

对于基本的Upsert我很满意,但我想让实际的更新以另一列的值为条件。可以将其视为“锁定”一行,以便Upsert过程不会进行进一步的更新。我可以像这样更改UPDATE语句:

UPDATE dbo.Item WITH (SERIALIZABLE)
SET Title = @pTitle,
    Teaser = @pTeaser
WHERE ContentID = @pContentID
AND RowLocked = false

但是当后面的Insert尝试插入已经存在但由于“已锁定”而未更新的行时,后续插入将因唯一约束违规(针对ContentID字段)而失败。

这是否意味着我不再拥有经典的Upsert,即我每次都必须选择行来确定它是否可以更新或插入?我认为是这种情况,所以我想我真正要求的是帮助确保事务隔离级别正确,以便程序安全执行。

6 个答案:

答案 0 :(得分:9)

一个非常常见的问题。有些方法在高并发性下并不成功。这里描述和压力测试:

Stress testing UPSERTs

Defensive database programming: eliminating IF statements.

在这种情况下,只编写一些代码是不够的,您需要公开它 高并发性。例如,我不确定我是否了解CptSkippy 建议,但以下演示了如何进行压力测试。设置表格和程序:

CREATE TABLE [dbo].[TwoINTs](
      [ID] [int] NOT NULL,
      [i1] [int] NOT NULL,
      [i2] [int] NOT NULL,
      [i3] [int] NOT NULL
);
CREATE PROCEDURE dbo.SaveTwoINTs(@ID INT, @i1 INT, @i2 INT)
AS
BEGIN
      SET NOCOUNT ON;
      SET XACT_ABORT OFF;
      SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
      DECLARE @ret INT;
      SET @ret=0;
      BEGIN TRAN; 
IF EXISTS(SELECT 1 FROM dbo.TwoINTs WHERE ID=@ID) BEGIN
      UPDATE dbo.TwoINTs WITH (SERIALIZABLE)
         SET i1=i1+@i1, i2=i2+@i2 WHERE ID=@ID;
      SET @ret=@@ERROR;
END ELSE BEGIN
     INSERT INTO dbo.TwoINTs(ID, i1, i2, i3)VALUES(@ID, @i1, @i2, @i1);
      SET @ret=@@ERROR;
END;
COMMIT;
RETURN @ret;
END
GO

设置两个执行该过程的循环:

CREATE PROCEDURE Testers.UpsertLoop1
AS
BEGIN
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT;
SET @count = 0;
WHILE @count<50000 BEGIN
      SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts;
    EXEC @ret=dbo.SaveTwoINTs @ID, 1, 0;
      SET @count = @count + 1;
END;
END;
GO
CREATE PROCEDURE Testers.UpsertLoop2
AS
BEGIN
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT;
SET @count = 0;
WHILE @count<50000 BEGIN
      SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts;
    EXEC @ret=dbo.SaveTwoINTs @ID, 0, 1;
      SET @count = @count + 1;
END;
END;

在两个标签中执行这些程序,亲眼看看你会遇到很多错误:

Testers.UpsertLoop1 --run in one tab
Testers.UpsertLoop1 --run in one tab

Msg 2601, Level 14, State 1, Procedure SaveTwoINTs, Line 15
Cannot insert duplicate key row in object 'dbo.TwoINTs' with unique index 'UNQ_TwoInts_ID'.
The statement has been terminated.

按照我提供的链接查看在并发下实际工作的方法。

答案 1 :(得分:2)

我打了下面的脚本来证明我过去几年使用的这个技巧。如果您使用它,您需要修改它以适合您的目的。评论如下:

/*
CREATE TABLE Item
 (
   Title      varchar(255)  not null
  ,Teaser     varchar(255)  not null
  ,ContentId  varchar(30)  not null
  ,RowLocked  bit  not null
)


UPDATE item
 set RowLocked = 1
 where ContentId = 'Test01'

*/


DECLARE
  @Check varchar(30)
 ,@pContentID varchar(30)
 ,@pTitle varchar(255)
 ,@pTeaser varchar(255)

set @pContentID = 'Test01'
set @pTitle     = 'TestingTitle'
set @pTeaser    = 'TestingTeasier'

set @check = null

UPDATE dbo.Item
 set
   @Check = ContentId
  ,Title  = @pTitle
  ,Teaser = @pTeaser
 where ContentID = @pContentID
  and RowLocked = 0

print isnull(@check, '<check is null>')

IF @Check is null
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked)
     values (@pContentID, @pTitle, @pTeaser, 0)

select * from Item

这里的技巧是你可以在Update语句中设置局部变量中的值。上面,只有在更新有效时(即满足更新条件),才会设置“flag”值;否则,它将不会被更改(此处,保留为null),您可以检查它,并相应地进行处理。

至于交易并使其可序列化,我想知道在建议如何进行之前必须在交易中封装的内容。

- 补遗,以下第二条评论的后续行动-----------

先生。 Saffron的想法是实现此例程的彻底而可靠的方法,因为您的主键在外部定义并传递到数据库中(即您没有使用标识列 - 我很好,它们经常被过度使用)。

我做了一些测试(在列ContentId上添加了一个主键约束,在事务中包装了UPDATE和INSERT,将可序列化的提示添加到更新中)是的,那应该做你想要的一切。失败的更新会对索引的该部分进行范围锁定,并且会阻止同时尝试在列中插入新值。当然,如果同时提交N个请求,“first”将创建该行,并且它将立即由第二个,第三个等更新 - 除非您在该行的某处设置“lock”。好招!

(请注意,如果没有键列上的索引,则会锁定整个表。此外,范围锁定可能会锁定新值“任一侧”的行 - 或者可能不会,I没有测试那一个。无所谓,因为操作的持续时间[?]应该是一位数毫秒。)

答案 2 :(得分:1)

BEGIN TRANSACTION

IF EXISTS(SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
     UPDATE dbo.Item WITH (SERIALIZABLE)
     SET Title = @pTitle, Teaser = @pTeaser
     WHERE ContentID = @pContentID
     AND RowLocked = false
ELSE
     INSERT INTO dbo.Item
          (ContentID, Title, Teaser)
     VALUES
          (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION

答案 3 :(得分:0)

您可以切换更新/插入的顺序。因此,您在try / catch中执行插入操作,如果遇到约束违规,请执行更新。虽然觉得有点脏。

答案 4 :(得分:0)

CREATE PROCEDURE [dbo]。[usp_UpsertItem]      - 在此处添加存储过程的参数     @pContentID varchar(30)= null,     @pTitle varchar(255)= null,     @pTeaser varchar(255)= null 如 开始      - 添加SET NOCOUNT ON以防止出现额外的结果集      - 干扰SELECT语句。     SET NOCOUNT ON;

BEGIN TRANSACTION
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID
             AND RowLocked = false)
       UPDATE dbo.Item 
       SET Title = @pTitle, Teaser = @pTeaser
       WHERE ContentID = @pContentID
             AND RowLocked = false
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION

END

答案 5 :(得分:-2)

我放弃了交易。

另外@@ rowcount可能会起作用,但使用全局变量作为条件检查会导致错误。

只需进行存在()检查。无论如何你必须通过表格,所以速度不是问题。

就我所见,无需进行交易。