Reputation: 53
I have the problem that I'm getting multiple inserted rows where only 1 row should be inserted.
I have a procedure similar to this but the procedure is being executed multiple times with the same input parameters at the same moment by different sources so we are getting multiple duplicated lines with the same data.
Edit: more info, we are getting multiple different events and each event might execute this procedure if it has a new server_id that has not been added to the table before. Sometimes we get multiple events that all have the same server_id which has not yet been entered into the database so they all try to enter it into the db with this procedure at the same time and we get multiple rows in the table
CREATE PROCEDURE [stuff].[p_insert_missing_data]
@in_event_timestamp DATETIME2(7),
@in_source_system NVARCHAR(50),
@in_server_id NVARCHAR(50)
AS
BEGIN
INSERT INTO house.servers (
xtract_datetime,
xtract_source,
server_id
)
SELECT GETDATE() AS xtract_datetime,
@in_source_system AS xtract_source,
@in_server_id AS server_id
WHERE
NOT EXISTS (SELECT 1 FROM house.servers WHERE server_id = @in_server_id)
END
Upvotes: 1
Views: 1584
Reputation: 72060
To ensure you do not get any chance of a race condition, where multiple processes try to insert the same row, you need SERIALIZABLE
isolation level. You can set this on the whole procedure, or better is to add an explicit hint to just that table reference WITH (SERIALIZABLE)
or WITH (HOLDLOCK)
. The server will then place a key range lock, which not only locks that key, it also locks the absence of the key. It will also hold that lock to the end of the transaction (statement if no transaction).
This has the downside that there is more locking, increasing the risk of deadlocks. Therefore you should also add a UPDLOCK
hint.
INSERT INTO house.servers (
xtract_datetime,
xtract_source,
server_id
)
SELECT GETDATE(),
@in_source_system,
@in_server_id
WHERE
NOT EXISTS (SELECT 1
FROM house.servers s WITH (SERIALIZABLE, UPDLOCK)
WHERE s.server_id = @in_server_id);
You do not need a transaction, as every statement implicitly runs in its own transaction anyway.
For this to only place a key lock, and not lock the whole table up, you also need an index. This can either be clustered or non-clustered, although server_id
I'm guessing is the primary key, therefore should normally be clustered anyway.
house.servers (server_id ASC)
Upvotes: 1