Only inserting a row if it's not already there
Solution 1:
What about the "JFDI" pattern?
BEGIN TRY
INSERT etc
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
END CATCH
Seriously, this is quickest and the most concurrent without locks, especially at high volumes. What if the UPDLOCK is escalated and the whole table is locked?
Read lesson 4:
Lesson 4: When developing the upsert proc prior to tuning the indexes, I first trusted that the
If Exists(Select…)
line would fire for any item and would prohibit duplicates. Nada. In a short time there were thousands of duplicates because the same item would hit the upsert at the same millisecond and both transactions would see a not exists and perform the insert. After much testing the solution was to use the unique index, catch the error, and retry allowing the transaction to see the row and perform an update instead an insert.
Solution 2:
I added HOLDLOCK which wasn't present originally. Please disregard the version without this hint.
As far as I'm concerned, this should be enough:
INSERT INTO TheTable
SELECT
@primaryKey,
@value1,
@value2
WHERE
NOT EXISTS
(SELECT 0
FROM TheTable WITH (UPDLOCK, HOLDLOCK)
WHERE PrimaryKey = @primaryKey)
Also, if you actually want to update a row if it exists and insert if it doesn't, you might find this question useful.
Solution 3:
You could use MERGE:
MERGE INTO Target
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2)
ON Target.key = Source.key
WHEN MATCHED THEN
UPDATE SET value1 = Source.value1, value2 = Source.value2
WHEN NOT MATCHED BY TARGET THEN
INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2)
Solution 4:
Firstly, huge shout out to our man @gbn for his contributions to the community. Can't even begin to explain how often I find myself following his advice.
Anyway, enough fanboy-ing.
To add slightly to his answer, perhaps "enhance" it. For those, like me, left feeling unsettled with what to do in the <> 2627
scenario (and no an empty CATCH
is not an option). I found this little nugget from technet.
BEGIN TRY
INSERT etc
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
BEGIN
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT @ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE();
RAISERROR (
@ErrorMessage,
@ErrorSeverity,
@ErrorState
);
END
END CATCH
Solution 5:
I don't know if this is the "official" way, but you could try the INSERT
, and fall back to UPDATE
if it fails.