249

I need to write a T-SQL stored procedure that updates a row in a table. If the row doesn't exist, insert it. All this steps wrapped by a transaction.

This is for a booking system, so it must be atomic and reliable. It must return true if the transaction was committed and the flight booked.

I'm new to T-SQL, and not sure on how to use @@rowcount. This is what I've written until now. Am I on the right road? I'm sure is an easy problem for you.

-- 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?)
Whymarrh
  • 11,635
  • 13
  • 55
  • 96
  • 1
    possible duplicate of [Solutions for INSERT OR UPDATE on SQL Server](http://stackoverflow.com/questions/108403/solutions-for-insert-or-update-on-sql-server) – Alex Angas Jun 27 '11 at 05:47
  • related question - https://stackoverflow.com/questions/21889843/unique-constraint-vs-checking-before-insert – Steam Feb 21 '14 at 21:30

11 Answers11

169

I assume a single row for each flight? If so:

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

I assume what I said, as your way of doing things can overbook a flight, as it will insert a new row when there are 10 tickets max and you are booking 20.

Gregory A Beamer
  • 16,342
  • 3
  • 23
  • 29
  • Yes. There is 1 row per flight. But your code do the SELECT but doesn't check if the flight is full before to UPDATE. How to do this? –  Mar 12 '09 at 18:31
  • 3
    Due to race conditions it's only correct if current transaction isolation level is Serializable. – Jarek Przygódzki Sep 27 '11 at 11:53
  • 1
    @Martin: The answer was focused on the question at hand. From the OP's own statement "All this steps wrapped by a transaction". If the transaction is implemented correctly, the thread safe issue should not be an issue. – Gregory A Beamer Sep 27 '11 at 14:37
  • 14
    @GregoryABeamer - Simply sticking it in a `BEGIN TRAN ... COMMIT` under default isolation level won't resolve the issue. The OP specified that **atomic and reliable** were requirements. Your answer fails to address this in any shape or form. – Martin Smith Sep 27 '11 at 14:49
  • @MartinSmith - Is there a simple modification which would ensure that his answer becomes thread safe ? – Steam Feb 21 '14 at 21:21
  • @MartinSmith - I made a question related to that. The problem is that I am inserting millions of rows and I want to wrap Cassius's code aroun each insert satatement. I don't know if I am doing the righ thing. Can you please help me ? The question is here - https://stackoverflow.com/questions/21945850/a-thread-safe-way-to-check-if-a-row-exists-before-inserting-is-my-code-correct – Steam Feb 21 '14 at 21:59
  • 2
    Would this be thread-safe if (UPDLOCK, HOLDLOCK) was added to the SELECT: `IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)`? – Jim Nov 25 '16 at 11:08
163

Take a look at MERGE command. You can do UPDATE, INSERT & DELETE in one statement.

Here is a working implementation on using MERGE
- It checks whether flight is full before doing an update, else does an insert.

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

And then ...

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
potashin
  • 42,140
  • 11
  • 76
  • 100
dance2die
  • 31,758
  • 34
  • 122
  • 177
  • 6
    Also, see why you might like [WITH (HOLDLOCK)](http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx) for that MERGE. – Eugene Ryabtsev Jun 27 '13 at 04:34
  • 4
    I think MERGE is supported after 2005 (so 2008+). – samis Dec 04 '13 at 15:04
  • 4
    MERGE without WITH(UPDLOCK) can have primary key violations, which would be bad in this case. See [Is MERGE an atomic statement in SQL2008?] (http://stackoverflow.com/questions/9871644/is-merge-an-atomic-statement-in-sql2008) – James Feb 03 '15 at 22:41
68

Pass updlock, rowlock, holdlock hints when testing for existence of the row.

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 */

The updlock hint forces the query to take an update lock on the row if it already exists, preventing other transactions from modifying it until you commit or roll back.

The holdlock hint forces the query to take a range lock, preventing other transactions from adding a row matching your filter criteria until you commit or roll back.

The rowlock hint forces lock granularity to row level instead of the default page level, so your transaction won't block other transactions trying to update unrelated rows in the same page (but be aware of the trade-off between reduced contention and the increase in locking overhead - you should avoid taking large numbers of row-level locks in a single transaction).

See http://msdn.microsoft.com/en-us/library/ms187373.aspx for more information.

Note that locks are taken as the statements which take them are executed - invoking begin tran doesn't give you immunity against another transaction pinching locks on something before you get to it. You should try and factor your SQL to hold locks for the shortest possible time by committing the transaction as soon as possible (acquire late, release early).

Note that row-level locks may be less effective if your PK is a bigint, as the internal hashing on SQL Server is degenerate for 64-bit values (different key values may hash to the same lock id).

Cassius Porcus
  • 751
  • 5
  • 3
  • 4
    Locking is VERY important to avoid overbooking. Is it correct to assume that a lock declared in the IF statement is held until the end of the IF statement, i.e. for one update statement? Then it might be wise to show the code above using begin end block markers to prevent newbies from copy&pasting your code and still getting it wrong. – Simon B. Oct 07 '10 at 17:14
  • Is there a problem if my PK is a varchar (NOT max though) or a combination of three VARCHAR columns ? – Steam Feb 21 '14 at 21:30
  • I made a question related to this answer at - https://stackoverflow.com/questions/21945850/a-thread-safe-way-to-check-if-a-row-exists-before-inserting-is-my-code-correct Question is can this code be used for inserting millions of rows. – Steam Feb 21 '14 at 22:00
  • This solution would impose too much locking overhead in cases when many threads often test already existing rows. I guess this can be worked around with a kind of double checked locking via preventive extra `exists` check without locking hints. – Vadzim May 14 '18 at 19:36
40

i'm writing my solution. my method doesn't stand 'if' or 'merge'. my method is easy.

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

For Example:

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

Explanation:

(1) SELECT col1,col2 FROM TableName WHERE col1=@par1 AND col2=@par2 It selects from TableName searched values

(2) SELECT @par1, @par2 WHERE NOT EXISTS It takes if not exists from (1) subquery

(3) Inserts into TableName (2) step values

Cem
  • 736
  • 1
  • 7
  • 12
  • 1
    it is only for inserting, not updating. – Cem May 17 '17 at 15:07
  • 1
    It's actually still possible for this method to fail cause check for existence is done before the insert - see https://stackoverflow.com/a/3790757/1744834 – Roman Pekar May 15 '20 at 09:34
5

I finally was able to insert a row, on the condition that it didn't already exist, using the following model:

INSERT INTO table ( column1, column2, column3 )
(
    SELECT $column1, $column2, $column3
      WHERE NOT EXISTS (
        SELECT 1
          FROM table 
          WHERE column1 = $column1
          AND column2 = $column2
          AND column3 = $column3 
    )
)

which I found at:

http://www.postgresql.org/message-id/87hdow4ld1.fsf@stark.xeocode.com

Matthew Frontino
  • 486
  • 2
  • 12
Paul G
  • 709
  • 6
  • 10
2

This is something I just recently had to do:

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
TheTXI
  • 36,035
  • 10
  • 82
  • 110
1
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])
Almamun
  • 11
  • 3
1

You could use the Merge Functionality to achieve. Otherwise you can do:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....
Ry-
  • 199,309
  • 51
  • 404
  • 420
JoshBerke
  • 62,464
  • 23
  • 120
  • 162
0

Full solution is below (including cursor structure). Many thanks to Cassius Porcus for the begin trans ... commit code from posting above.

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
sashkello
  • 15,161
  • 19
  • 74
  • 101
-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table
Dmitriy
  • 5,347
  • 12
  • 23
  • 37
Aaron
  • 11
  • INSERT INTO table ( column1, column2, column3 ) SELECT $column1, $column2, $column3 EXCEPT SELECT column1, column2, column3 from table – Aaron Mar 27 '15 at 22:47
  • 1
    There are a lot of highly upvoted answers to this question. Could you elaborate to explain what this answer add to the existing answers ? – francis Mar 28 '15 at 16:55
-2

The best approach to this problem is first making the database column UNIQUE

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name ,the value won't be inserted if it results in a duplicate key/already exists in the table.

Maurice Elagu
  • 714
  • 8
  • 20