Deadlock on inserting a new row - sql

I have a table that stores the keys of other tables for a many-to-many relationship. Example:
create table for_example(a uuid, b uuid, primary key(a, b));
Multiple transactions can insert data into a table at the same time. In this case, it may turn out that two transactions are inserting the same data. In this case, one of these transactions will see that the record already exists and will wait for the parallel transaction to complete its work in order to continue its work. But it may turn out that before the lock occurred, the second transaction had already inserted any data into this table, and in this case, if the first transaction tries to insert new data that the second transaction has already inserted, a deadlock will occur. Example:
begin;
--check exists row in table before insert in condition "IF"
select 1 from for_example where a = '002e202d-c564-45cd-a0a7-d8fe8b930d99' and b = '4ab9d0ed-80c7-42ee-a641-59834a61f06d';
--if not exists then insert
insert into for_example(a, b)
values('002e202d-c564-45cd-a0a7-d8fe8b930d99', '4ab9d0ed-80c7-42ee-a641-59834a61f06d')
ON CONFLICT DO NOTHING;
we pass at this time to the second transaction
begin;
--check exists row in table before insert in condition "IF"
select 1 from for_example where a = '4ab9d0ed-80c7-42ee-a641-59834a61f06d' and b = '002e202d-c564-45cd-a0a7-d8fe8b930d99';
--if not exists then insert
insert into for_example(a, b)
values('4ab9d0ed-80c7-42ee-a641-59834a61f06d', '002e202d-c564-45cd-a0a7-d8fe8b930d99')
ON CONFLICT DO NOTHING;
we return to the first transaction and try to insert the data that the second transaction has already inserted
--check exists row in table before insert in condition "IF"
select 1 from for_example where a = '4ab9d0ed-80c7-42ee-a641-59834a61f06d' and b = '002e202d-c564-45cd-a0a7-d8fe8b930d99';
--if not exists then insert
insert into for_example(a, b)
values('4ab9d0ed-80c7-42ee-a641-59834a61f06d', '002e202d-c564-45cd-a0a7-d8fe8b930d99')
ON CONFLICT DO NOTHING;
blocking occurs, we are waiting for the second transaction to finish its work, but at this time the second transaction is trying to insert data that the first transaction has already inserted
--check exists row in table before insert in condition "IF"
select 1 from for_example where a = '002e202d-c564-45cd-a0a7-d8fe8b930d99' and b = '4ab9d0ed-80c7-42ee-a641-59834a61f06d';
--if not exists then insert
insert into for_example(a, b)
values('002e202d-c564-45cd-a0a7-d8fe8b930d99', '4ab9d0ed-80c7-42ee-a641-59834a61f06d')
ON CONFLICT DO NOTHING;
error occurs: [40P01] ERROR: deadlock detected.
Is it possible to somehow check for the existence of a row in a table with the condition that it may already have been inserted by a parallel transaction?
This is a real issue we're having on a production server right now.

Related

If my for update trigger raise error, my update statement should fail

To ensure version control, I created a For Update trigger on my table. I have two tables. Account table, step one Second, the Account history table, which is utilized in the trigger, has a column called Version. If any of my columns are modified, I have Version+1 written in the column, and the old record from the Account table will be inserted in the Account history in the trigger. Additionally, I have a trigger new condition written. The newer version ought to be grated. version, If I run an update query on my main (Account) table to perform negative testing while keeping the older version, I get a trigger-defined error, but my update statement still updates the Account table, even though it shouldn't. I need to add transaction(BEGIN TRY BEGIN CATCH TRAN) on my update?, If my trigger fails my update statement should fail
ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
BEGIN
SELECT old.column
FROM deleted
SELECT new.Version
FROM inserted
SELECT old.Version FROM deleted
IF #Old_Version >= #New_Version
BEGIN
RAISERROR ('Improper version information provided',16,1);
END
ELSE
BEGIN
INSERT INTO AccountHistory
(
insert column
)
VALUES
(
old.column
);
END
END
UPDATE account
SET id= 123456,
Version = 1
WHERE id =1
Instead of using RAISERROR, you should use THROW. This will respect XACT_ABORT and automatically rollback the transaction.
You also have other fatal flaws in your trigger:
It expects there to be exactly one row modified. It may be multiple or zero rows.
You have not declared any variables and are instead selecting back out to the client.
Either way, you should just join the inserted and deleted tables by primary key.
CREATE OR ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted) -- early bailout
RETURN;
IF EXISTS (SELECT 1
FROM inserted i
JOIN deleted d ON d.YourPrimaryKey = i.YourPrimaryKey
WHERE d.Version >= i.Version
)
THROW 50001, 'Improper version information provided', 1;
INSERT INTO AccountHistory
(
insert column
)
SELECT
columsHere
FROM deleted;

Modify two tables (insert or update) based on existance of a row in the first table

I have a simple thing to do but somehow can't figure out how to do it.
I have to modify two tables (insert or update) based on existance of a row in the first table.
There is a possibility that some other process will insert the row with id = 1
between getting the flag value and "if" statement that examines its value.
The catch is - I have to change TWO tables based on the flag value.
Question: How can I ensure the atomicity of this operation?
I could lock both tables by "select with TABLOCKX", modify them and release the lock by committing the transaction but ... won't it be overkill?
declare #flag int = 0
begin tran
select #flag = id from table1 where id = 1
if #flag = 0
begin
insert table1(id, ...) values(1, ...)
insert table2(id, ...) values(1, ...)
end
else
begin
update table1 set colX = ... where id = 1
update table2 set colX = ... where id = 1
end
commit tran
To sumarize our conversation and generalize to other's case :
If your column [id] is either PRIMARY KEY or UNIQUE you can put a Lock on that row. No other process will be able to change the value of [id]
If not, in my opinion you won't have other choice than Lock the table with a TABLOCKX. It will prevent any other process to UPDATE,DELETE or INSERT a row.
With that lock, it could possibly allow an other process to SELECT over the table depending on your isolation level.
If your database is in read_committed_snapshot, the other process would read the "old" value of the same [id].
To check your isolation level you can run
SELECT name, is_read_committed_snapshot_on FROM sys.databases

SQL Server trigger validation to insert or update

I have to develop a trigger for multiple value update for the Change_Table that contains two columns Article_C (primary key) and Status_C. The trigger activates when the Status_C column is updated and it needs to either insert or update the Target_Tablewhich columns are Article_T (primary key) and Status_T.
The Status column for both tables is a NOT NULL int
For insert validation: insert all Change_Table values (Articles & Status) that do not exist into the Target_Table.
For update condition: if the articles exist in the Target_Table, pass the updated status value to that table.
Status values are 1 or 2.
My current implementation:
CREATE TRIGGER [dbo].[Article_Trigger]
ON [dbo].[Change_Table]
AFTER UPDATE
NOT FOR REPLICATION
AS
IF UPDATE (Status_C)
BEGIN
INSERT INTO Target_Table (Article_T, Status_T)
SELECT Article_C, Status_C
FROM Change_Table
WHERE NOT EXISTS (SELECT * FROM Target_Table
WHERE Change_Table.Article_C = Target_Table.Article_T
AND Change_Table.Status_C = Target_Table.Status_T)
END
This is a rough idea of the insert but it only works once when the Change_Table is first updated and the target table is empty. After that I get an error "Violation of PRIMARY KEY constraint" due to duplicate primary key after updating the status for a second time due to the insert condition.
Query that gave error after second execution:
update Change_Table set status_c = 1 where Article_C in (1000,1003)
How can I modify this query to implement the update status condition to this trigger?
I think you could use deleted or inserted tables. These 2 virtual tables are working nicely with trigger. In you example, you may not need deleted table as you are not logging the historical changes.
CREATE TRIGGER [dbo].[Article_Trigger]
ON [dbo].[Change_Table]
AFTER UPDATE
NOT FOR REPLICATION
AS
IF UPDATE (Status_C)
BEGIN
--insert if not exist
INSERT INTO Target_Table
SELECT A.Article_C, A.Status_C
FROM inserted as A --the updated records from the source table will exist in the inserted virtual table
LEFT JOIN Target_Table as B
ON B.Article_T = A.Article_C
WHERE B.Article_T IS NULL
--update if exist
UPDATE A
SET A.Status_T = B.Article_C
FROM Target_Table as A
INNER JOIN inserted as B --the updated records from the source table will exist in the inserted virtual table
ON B.Article_C = A.Article_T
END

Table locked after a quick INSERT and UPDATE of 1 record

I executed a few queries like below, where it INSERTS 1 row to the table then UPDATE it, and I didn't get any error.
But then I found out that it locked the table, where no one else can query the table.
Do you know why the query below would lock the table ?
Can I not SET vchB = vchNumber, vchC = vchNumber right after INSERT INTO ?
I read when/what locks are hold/released in READ COMMITTED isolation level, and it says "All lock will release only after committed/rollbacked".
The INSERT and UPDATE statements below were committed succesfully (it only INSERT and UPDATE 1 row in the table), and it only took a second to run, but yet when other people query the table, it just hang.
Thank you.
BEGIN TRANSACTION T1
INSERT INTO myTbl(vchSN,vchNumber,vchName)
SELECT 'AB12','1234','My Name'
UPDATE myTbl
SET vchB = vchNumber, vchC = vchNumber, vchtab = 'N' where vchSN = 'AB12'
COMMIT TRANSACTION T1

UPDATE and INSERT should fire trigger only once

Is there any way to combine an update and an insert statements in a way that they fires a trigger only once?
I have one particular table that has (and currently needs) a trigger AFTER INSERT, UPDATE, DELETE. Now I want to update one row and insert another row and have the trigger fire only once for that.
Is this at all possible?
I already tried a MERGE-Statement without success: The trigger fires once for the update- and once for the insert-part.
Well, problem solved for me. I did NOT find a way to combine the statements into one fire-event of the trigger. But the trigger behaves in an interesting way, that was good enough for me: Both calls to the trigger do already have access to the fully updated data.
Just execute the following statements and you will see what I mean.
CREATE TABLE Foo (V INT)
GO
CREATE TRIGGER tFoo ON Foo AFTER INSERT, UPDATE, DELETE
AS
SELECT 'inserted' AS Type, * FROM inserted
UNION ALL
SELECT 'deleted', * FROM deleted
UNION ALL
SELECT 'actual', * FROM Foo
GO
DELETE FROM Foo
INSERT Foo VALUES (1)
;MERGE INTO Foo
USING (SELECT 2 AS V) AS Source ON 1 = 0
WHEN NOT MATCHED BY SOURCE THEN DELETE
WHEN NOT MATCHED BY TARGET THEN INSERT (V) VALUES (Source.V);
As a result, the trigger will be called twice for the MERGE. But both times, "SELECT * FROM Foo" delivers the fully updated data already: There will be one row with the value 2. The value 1 is deleted already.
This really surprised me: The insert-trigger is called first and the deleted row is gone from the data before the call to the delete-trigger happens.
Only the values of "inserted" and "deleted" correspond to the delete- or insert-statement.
You could try something like this:
The trigger would check for the existence of #temp table.
If it doesn't exist, it creates it with dummy data. It then checks if the recent values contain the same user (SPID) that is running now and if the last time it was triggered was within 20 seconds.
If these are true then it will PRINT 'Do Nothing' and drop the table, otherwise it will do your trigger statement.
At the end of your trigger statement it inserts into the table the SPID and current datetime.
This temp table should last as long as the SPID connection, if you want it to last longer make it a ##temp or a real table.
IF OBJECT_ID('tempdb..#temp') IS NULL
begin
Create table #temp(SPID int, dt datetime)
insert into #temp values (0, '2000-01-01')
end
If ##SPID = (select top 1 SPID from #temp order by dt desc)
and Convert(datetime,Convert(varchar(19),GETDATE(),121)) between
Convert(datetime,Convert(varchar(19),(Select top 1 dt from #temp order by dt desc),121)) and
Convert(datetime,Convert(varchar(19),DateAdd(second, 20, (select top 1 dt from #temp order by dt desc)),121))
begin
PRINT 'Do Nothing'
Drop table #temp
end
else
begin
--trigger statement
Insert into #temp values (##SPID, GETDATE())
end