Adding or Updating Entries in a Lookup Table
♦ Accept the lookup key and value as input arguments and return the unique id of the new (or existing) key that was just inserted (or updated) as an output argument.
♦ Correctly deal with race conditions where two separate entities (processes, threads, etc…) are trying to insert or update the same lookup key/value pair and accomplish this without using a transaction.
For this discussion, we’ll assume that our lookup table contains a list of performance counters. The (reasonably) well commented T-SQL code below should be self-explanatory:
CREATE PROCEDURE [dbo].[usp_PutPerfCounter]
(
@PerfCounterName varchar(50),
@PerfCounterValue int output
) AS BEGIN
DECLARE @nRowCount int
DECLARE @nErr int
DECLARE @nErrSeverity int
DECLARE @strErrMsg nvarchar(4000)
DECLARE @nIns int
DECLARE @nErrState int
DECLARE @nExisting int
SELECT @nErr = 0, @nIns = 0, @nErrSeverity = 0, @nErrState = 0
-- Make sure that @PerfCounterName argument is valid
IF @PerfCounterName IS NULL OR LEN(@PerfCounterName) = 0
BEGIN
RAISERROR('Perf counter name must be provided.',16,1)
END
ELSE
BEGIN
SELECT @nExisting = COUNT(DISTINCT PerfCounterName)
FROM PerfCounter WHERE PerfCounterName = @PerfCounterName
-- Check to see if the there is already an existing
-- entry with the same key.
IF(@nExisting = 0)
BEGIN
-- The try/catch feature is new in SQL Server 2005 and
-- here we use it to squash exceptions generated due to
-- a potential unique constraint violation error because
-- our intention is to handle this particular error within
-- the stored proc without passing it on to the application
-- layer.
BEGIN TRY
INSERT INTO
[dbo].[PerfCounter](PerfCounterName, PerfCounterValue)
VALUES
(@PerfCounterName, @PerfCounterValue )
SELECT @nIns = 1
END TRY
BEGIN CATCH
SELECT @nErr = @@ERROR
SELECT @strErrMsg = ERROR_MESSAGE()
SELECT @nErrSeverity = ERROR_SEVERITY()
SELECT @nErrState = ERROR_STATE()
-- A value of 2627 for @nErr means that
-- between the time we ruled out a pre-existing
-- entry with the same key and our attempt to
-- insert (what we thought) was a new entry,
-- another entity has already successfully inserted
-- a new entry with the same key that we're trying to insert.
-- We'll let the first successful insert stick and just
-- return that value in the input/output parameter
-- (@PerfCounterValue).
IF @nErr = 2627 -- 2627 stands for unique constraint violation
BEGIN
SELECT @PerfCounterValue = PerfCounterValue
FROM [dbo].[PerfCounter]
WHERE PerfCounterName = @PerfCounterName
END
ELSE IF( @nErr <> 0 ) -- Some other occurred...
BEGIN
RAISERROR(@strErrMsg,@nErrSeverity,@nErrState)
END
END CATCH
END
END
-- Update the PerfCounterValue - assuming we haven't
-- already inserted another value.
IF( @nIns = 0)
BEGIN
UPDATE [dbo].[PerfCounter]
SET PerfCounterValue = @PerfCounterValue
WHERE PerfCounterName = @PerfCounterName
END
END
NOTE: As mentioned in the comments embedded in the T-SQL code, this technique relies on the new try/catch feature available only in SQL Server 2005 (and beyond, I assume).
NOTE: In this example, I chose to go with a policy of letting the value inserted by the first successful process stick in the event of a race condition. This could easily be changed if the application calls for the value associated with the last successful insert to stick.
