检查是否存在行,否则插入


237

我需要编写一个T-SQL存储过程来更新表中的一行。如果该行不存在,则将其插入。所有这些步骤都由事务包装。

这是用于预订系统的,因此它必须是原子性和可靠的。如果已提交交易并预订了航班,则必须返回true。

我是T-SQL的新手,不确定如何使用@@rowcount。这是我到目前为止所写的。我在正确的道路上吗?我相信这对您来说是一个简单的问题。

-- BEGIN TRANSACTION (HOW TO DO?)

UPDATE Bookings
 SET TicketsBooked = TicketsBooked + @TicketsToBook
 WHERE FlightId = @Id AND TicketsMax < (TicketsBooked + @TicketsToBook)

-- Here I need to insert only if the row doesn't exists.
-- If the row exists but the condition TicketsMax is violated, I must not insert 
-- the row and return FALSE

IF @@ROWCOUNT = 0 
BEGIN

 INSERT INTO Bookings ... (omitted)

END

-- END TRANSACTION (HOW TO DO?)

-- Return TRUE (How to do?)


Answers:


158

看一下MERGE命令。你可以做UPDATEINSERTDELETE在一个声明。

这是使用的有效实现MERGE
-在进行更新之前检查飞行是否已满,否则进行插入。

if exists(select 1 from INFORMATION_SCHEMA.TABLES T 
              where T.TABLE_NAME = 'Bookings') 
begin
    drop table Bookings
end
GO

create table Bookings(
  FlightID    int identity(1, 1) primary key,
  TicketsMax    int not null,
  TicketsBooked int not null
)
GO

insert  Bookings(TicketsMax, TicketsBooked) select 1, 0
insert  Bookings(TicketsMax, TicketsBooked) select 2, 2
insert  Bookings(TicketsMax, TicketsBooked) select 3, 1
GO

select * from Bookings

然后 ...

declare @FlightID int = 1
declare @TicketsToBook int = 2

--; This should add a new record
merge Bookings as T
using (select @FlightID as FlightID, @TicketsToBook as TicketsToBook) as S
    on  T.FlightID = S.FlightID
      and T.TicketsMax > (T.TicketsBooked + S.TicketsToBook)
  when matched then
    update set T.TicketsBooked = T.TicketsBooked + S.TicketsToBook
  when not matched then
    insert (TicketsMax, TicketsBooked) 
    values(S.TicketsToBook, S.TicketsToBook);

select * from Bookings

6
另外,请查看为什么您可能喜欢该合并的WITH(HOLDLOCK)
Eugene Ryabtsev 2013年

4
我认为MERGE将在2005年后获得支持(因此2008+)。
samis 2013年

3
不带WITH(UPDLOCK)的MERGE可能会违反主键,在这种情况下这很糟糕。参见[MERGE是在SQL2008原子语句?(stackoverflow.com/questions/9871644/...
詹姆斯

156

我为每个航班假设一行?如果是这样的话:

IF EXISTS (SELECT * FROM Bookings WHERE FLightID = @Id)
BEGIN
    --UPDATE HERE
END
ELSE
BEGIN
   -- INSERT HERE
END

我假设我说的是,因为您的处事方式可能会使航班超额预订,因为当最多有10张票并且您预订20张票时,它将插入新的一行。


是。每个航班有1行。但是您的代码会执行SELECT,但是在进行UPDATE之前不会检查档位是否已满。这该怎么做?

2
由于竞争条件,只有当前事务隔离级别为“可序列化”才是正确的。
JarekPrzygódzki2011年

1
@马丁:答案集中在眼前的问题上。从OP自己的语句“所有这些步骤都由事务包裹”。如果正确实现了事务,则线程安全问题不应成为问题。
格雷戈里·比默

14
@GregoryABeamer-仅将其BEGIN TRAN ... COMMIT置于默认隔离级别下并不能解决问题。OP规定必须要有原子性和可靠性。您的答案无法以任何形式解决。
马丁·史密斯

2
如果将(UPDLOCK,HOLDLOCK)添加到SELECT中,这是否是线程安全的IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)
吉姆(Jim)

67

在测试行是否存在时传递updlock,rowlock,holdlock提示。

begin tran /* default read committed isolation level is fine */

if not exists (select * from Table with (updlock, rowlock, holdlock) where ...)
    /* insert */
else
    /* update */

commit /* locks are released here */

updlock提示会强制查询对行进行更新锁定(如果该更新已存在),从而防止其他事务修改该行,直到您提交或回滚为止。

holdlock提示会强制查询进行范围锁定,从而防止其他事务添加符合您的过滤条件的行,直到您提交或回滚为止。

行锁提示会强制将粒度锁定到行级别,而不是默认的页面级别,因此您的事务将不会阻止其他事务尝试更新同一页面中不相关的行(但要注意减少争用和增加页之间的权衡)锁定开销-您应该避免在单个事务中使用大量的行级锁定)。

有关更多信息,请参见http://msdn.microsoft.com/en-us/library/ms187373.aspx

请注意,锁是作为执行锁的语句执行的-调用begin tran并不能使您免于受到其他事物的束缚,而在获得锁之前先将其锁定。您应通过尽快提交事务(延迟获取,提前释放),尝试使SQL保持锁定的时间最短。

请注意,如果您的PK是bigint,则行级锁的有效性可能会降低,因为SQL Server上的内部散列会退化为64位值(不同的键值可能会散列到相同的锁id)。


4
锁定对于避免预订过多非常重要。假设在IF语句中声明的锁一直保持到IF语句的末尾(即对于一个更新语句)是否正确?然后,使用开始结尾块标记来显示上面的代码可能是明智的,以防止新手复制并粘贴您的代码并仍然出错。
西蒙·B。2010年

如果我的PK是varchar(虽然不是最大)还是三个VARCHAR列的组合会出现问题吗?
Steam

我在-stackoverflow.com/questions/21945850/…上提出了与此答案有关的问题,问题是该代码可用于插入数百万行。
Steam

在许多线程经常测试已经存在的行的情况下,此解决方案将施加过多的锁定开销。我猜这可以通过一种预防性的额外exists检查而无需锁定提示的双重检查锁定来解决。
Vadzim

38

我正在写我的解决方案。我的方法不支持“如果”或“合并”。我的方法很简单。

INSERT INTO TableName (col1,col2)
SELECT @par1, @par2
   WHERE NOT EXISTS (SELECT col1,col2 FROM TableName
                     WHERE col1=@par1 AND col2=@par2)

例如:

INSERT INTO Members (username)
SELECT 'Cem'
   WHERE NOT EXISTS (SELECT username FROM Members
                     WHERE username='Cem')

说明:

(1)从TableName中选择col1,col2 WHERE col1 = @ par1 AND col2 = @ par2从TableName中选择搜索值

(2)SELECT @ par1,@ par2哪里不存在(1)子查询中是否不存在

(3)插入TableName(2)步骤值


1
它仅用于插入,不用于更新。
Cem

这种方法实际上仍然有可能失败,原因是在插入之前完成了是否存在的检查-请参阅stackoverflow.com/a/3790757/1744834
Roman Pekar


2

这是我最近要做的事情:

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cjso_UpdateCustomerLogin]
    (
      @CustomerID AS INT,
      @UserName AS VARCHAR(25),
      @Password AS BINARY(16)
    )
AS 
    BEGIN
        IF ISNULL((SELECT CustomerID FROM tblOnline_CustomerAccount WHERE CustomerID = @CustomerID), 0) = 0
        BEGIN
            INSERT INTO [tblOnline_CustomerAccount] (
                [CustomerID],
                [UserName],
                [Password],
                [LastLogin]
            ) VALUES ( 
                /* CustomerID - int */ @CustomerID,
                /* UserName - varchar(25) */ @UserName,
                /* Password - binary(16) */ @Password,
                /* LastLogin - datetime */ NULL ) 
        END
        ELSE
        BEGIN
            UPDATE  [tblOnline_CustomerAccount]
            SET     UserName = @UserName,
                    Password = @Password
            WHERE   CustomerID = @CustomerID    
        END

    END

1

您可以使用合并功能来实现。否则,您可以执行以下操作:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....

0

完整的解决方案在下面(包括游标结构)。非常感谢Cassius Porcus提供了begin trans ... commit上面发布的代码。

declare @mystat6 bigint
declare @mystat6p varchar(50)
declare @mystat6b bigint

DECLARE mycur1 CURSOR for

 select result1,picture,bittot from  all_Tempnogos2results11

 OPEN mycur1

 FETCH NEXT FROM mycur1 INTO @mystat6, @mystat6p , @mystat6b

 WHILE @@Fetch_Status = 0
 BEGIN

 begin tran /* default read committed isolation level is fine */

 if not exists (select * from all_Tempnogos2results11_uniq with (updlock, rowlock, holdlock)
                     where all_Tempnogos2results11_uniq.result1 = @mystat6 
                        and all_Tempnogos2results11_uniq.bittot = @mystat6b )
     insert all_Tempnogos2results11_uniq values (@mystat6 , @mystat6p , @mystat6b)

 --else
 --  /* update */

 commit /* locks are released here */

 FETCH NEXT FROM mycur1 INTO @mystat6 , @mystat6p , @mystat6b

 END

 CLOSE mycur1

 DEALLOCATE mycur1
 go

0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])

-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table

插入表(column1,column2,column3)SELECT $ column1,column2,column3除了从表中选择column1,column2,column3
亚伦

1
这个问题有很多被高度评价的答案。您能详细解释一下此答案对现有答案的补充吗?
弗朗西斯

-2

解决此问题的最佳方法是首先使数据库列为UNIQUE

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name ,如果该值导致表中存在重复的键/已经存在,则不会插入该值。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.