我正在尝试实现您的基本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,即我每次都必须选择行来确定它是否可以更新或插入?我认为是这种情况,所以我想我真正要求的是帮助确保事务隔离级别正确,以便程序安全执行。
答案 0 :(得分:9)
一个非常常见的问题。有些方法在高并发性下并不成功。这里描述和压力测试:
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可能会起作用,但使用全局变量作为条件检查会导致错误。
只需进行存在()检查。无论如何你必须通过表格,所以速度不是问题。
就我所见,无需进行交易。