views:

55

answers:

2

I want to do the SELECT / INSERT version of an UPSERT. Below is a template of the existing code:

// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))

IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE)
BEGIN
   INSERT Table VALUES (@Value)
   SELECT @id = SCOPEIDENTITY()
END
ELSE
   SELECT @id = RowID FROM Table WHERE RowValue = @VALUE)

The query will be called from many concurrent sessions. My performance tests show that it will consistently throw primary key violations under a specific load.

Is there a high-concurrency method for this query that will allow it to maintain performance while still avoiding the insertion of data that already exists?

+1  A: 

Since you're on SQL 2008, you should consider the MERGE statement as an alternative

Ed Harper
I wrote up a MERGE version of this query, but there doesn't seem to be a clean way to check if a row was inserted or not. Would you use @@ROWCOUNT?
8kb
@8kb: Check out the OUTPUT clause: http://technet.microsoft.com/en-us/library/bb510625.aspx
Andomar
-1 Unless you can you give an example of how you'd use MERGE for an INSERT/SELECT. Especially if you can avoid a dummy update... Please see the edit to my answer.
gbn
+3  A: 

You can use LOCKs to make things SERIALIZABLE but this reduces concurrency. Why not try the common condition first ("mostly insert or mostly select") followed by safe handling of "remedial" action? That is, the "JFDI" pattern...

Mostly INSERTs expected (ball park 70-80%+):

Just try to insert. If it fails, the row has already been created. No need to worry about concurrency because the TRY/CATCH deals with duplicates for you.

BEGIN TRY
   INSERT Table VALUES (@Value)
   SELECT @id = SCOPEIDENTITY()
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
    ELSE -- only error was a dupe insert so must already have a row to select
      SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH

Mostly SELECTs:

Similar, but try to get data first. No data = INSERT needed. Again, if 2 concurrent calls try to INSERT because they both found the row missing the TRY/CATCH handles.

BEGIN TRY
   SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
   IF @@ROWCOUNT = 0
   BEGIN
       INSERT Table VALUES (@Value)
       SELECT @id = SCOPEIDENTITY()
   END
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
    ELSE
      SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH

The 2nd one appear to repeat itself, but it's highly concurrent. Locks would achieve the same but at the expense of concurrency...

Edit:

Why not to use MERGE...

If you use the OUTPUT clause it will only return what is updated. So you need a dummy UPDATE to generate the INSERTED table for the OUTPUT clause. If you have to do dummy updates with many calls (as implied by OP) that is a lot of log writes just to be able to use MERGE.

gbn
@gbn - why would you use your 2nd suggestion (for high concurrency) instead of the MERGE command (assuming the @8kb is using sql 2008+) ?
Pure.Krome
@Pure.Krome: because it's insert/select, not insert/update. YOu'll end up with a dummy UPDATE to use OUTPUT. Great.
gbn
@gbn - so we cannot check the SCOPE_IDENTITY to grab the _last_ / _most recent_ identity that was inserted .. assuming an insert happened? if it didn't, then u can use @@Rowcount to check that the update worked... ??
Pure.Krome
@Pure.Krome: it's *not* an UPDATE
gbn
@gbn - becuase merge does update then insert, you're saying that, because the posted said ONLY insert, then merge shouldn't be used .. because it will never update and SHOULDN'T update? (i think i've gotcha now) ...
Pure.Krome
@Pure.Krome: yes. MERGE is not appropriate in this situation
gbn
@gbn cheers :) :)
Pure.Krome
@gbn - yes, there are mainly selects. And my problem with MERGE was trying to figure how to determine if the record was inserted or not (and still do the select). But this solution solves the problem. Thanks!
8kb