Concurrent create or select statement failing with deadlock error - sql-server

Where running the following SQL statement currently (maybe 30 or 40 times at the same time) and is giving us deadlock errors but we're not sure which statements are interfering with each other. There are no indexes on the Things table besides a PK and a Fk to ThingTypes table. We're also wondering if adding an index to ThingTypeId and HourId will help resolve the issue. Lastly it's also safe for us to assume that {#thingTypeID and #hourID} are unique for all the concurrent queries and the if is only there for if this was re-run at a later period in time.
Transaction (Process ID 237) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction
Code:
IF NOT EXISTS(select top(1) id from [eddsdbo].[Things] (UPDLOCK)
where [ThingTypeID] = #thingTypeID and [HourID] = #hourID)
BEGIN
INSERT INTO [eddsdbo].[Things]
([ThingTypeID]
,[HourID])
VALUES
(#thingTypeID
,#hourID)
SELECT m.*, mt.SampleType
FROM [eddsdbo].[Things] as m
inner join [eddsdbo].[ThingTypes] as mt on mt.Id = m.ThingTypeID
WHERE m.ID = ##IDENTITY
END
ELSE
BEGIN
SELECT m.*, mt.SampleType
FROM [eddsdbo].[Things] as m
inner join [eddsdbo].[ThingTypes] as mt on mt.Id = m.ThingTypeID
where [ThingTypeID] = #thingTypeID and [HourID] = #hourID
END
We've been chasing down this issue for a while so any help is appreciated.

Not sure this will fix it
But do you you really need the WHERE m.ID = ##IDENTITY
IF NOT EXISTS(select top(1) id from [eddsdbo].[Things] (UPDLOCK)
where [ThingTypeID] = #thingTypeID and [HourID] = #hourID)
BEGIN
INSERT INTO [eddsdbo].[Things]
([ThingTypeID], [HourID])
VALUES (#thingTypeID, #hourID)
END
SELECT m.*, mt.SampleType
FROM [eddsdbo].[Things] as m
inner join [eddsdbo].[ThingTypes] as mt
on mt.Id = m.ThingTypeID
and [ThingTypeID] = #thingTypeID
and [HourID] = #hourID
A single statement is a transaction
I think this will have less overhead
DECLARE #t AS TABLE (id int identity primary key, thingTypeID int, hourID int);
declare #thingTypeID int = 1, #hourID int = 2;
insert into #t (thingTypeID, hourID)
values (#thingTypeID, #hourID);
select *
from #T
where thingTypeID = #thingTypeID and hourID = #hourID;
insert into #t (thingTypeID, hourID)
select #thingTypeID, #hourID
where not exists (select 1 from #t where thingTypeID = #thingTypeID and hourID = #hourID);
select *
from #T
where thingTypeID = #thingTypeID and hourID = #hourID;
set #thingTypeID = 1;
set #hourID = 3;
insert into #t (thingTypeID, hourID)
select #thingTypeID, #hourID
where not exists (select 1 from #t where thingTypeID = #thingTypeID and hourID = #hourID);
select *
from #T
where thingTypeID = #thingTypeID and hourID = #hourID;
select *
from #T
order by id;

Related

Searching and inserting data if not exist using stored procedure

I am trying to insert data using a stored procedure while searching based on Customer and AccountNumber. Is there any way I can write following code in shorter form? Should I create a stored procedure in database for this or just use this to insert from VB directly?
declare #Customer int ,#AccountNumber int
IF NOT EXISTS (SELECT * FROM Table_A
WHERE AccountNumber = #AccountNumber AND Customer = #Customer)
BEGIN
INSERT INTO Table_A
SELECT TOP 1 *
FROM Table_B
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
END
IF NOT EXISTS (SELECT * FROM Table_A
WHERE AccountNumber = #AccountNumber AND Customer = #Customer)
BEGIN
INSERT INTO Table_A
SELECT TOP 1 *
FROM Table_C
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
END
IF NOT EXISTS (SELECT * FROM Table_A
WHERE AccountNumber = #AccountNumber AND Customer = #Customer)
BEGIN
INSERT INTO Table_A
SELECT TOP 1 *
FROM Table_D
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
END
Combining all the comments here is some shorter code. Points to note:
Its recommended to always fully list the columns involved in an INSERT statement. Its clearer what is happening, and not going to cause issues if you have IDENTITY columns, or change your table definition in future.
TOP 1 without an ORDER BY is going to return random results which is not usually what you want.
INSERT INTO Table_A
SELECT *
FROM (
SELECT TOP 1 *
FROM Table_B
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
UNION ALL
SELECT TOP 1 *
FROM Table_C
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
UNION ALL
SELECT TOP 1 *
FROM Table_D
WHERE AccountNumber = #AccountNumber AND Customer = #Customer
) X
WHERE NOT EXISTS (
SELECT 1
FROM Table_A A
WHERE A.AccountNumber = X.AccountNumber AND A.Customer = X.Customer
)

Update in Batches Never Finishes

as a follow up on my question original question posted here
UPDATE in Batches Does Not End and Remaining Data Does Not Get Updated
If you use the logic below you'll see that update never finishes. Let me know if you have any ideas why...
Table 1
IF OBJECT_ID('tempdb..#Table2') IS NOT NULL
BEGIN
DROP TABLE #Table2;
END
CREATE TABLE #Table2 (ID INT);
DECLARE #Count int = 0;
WHILE (select count(*) from #Table2) < 10000 BEGIN
INSERT INTO #Table2 (ID)
VALUES (#Count)
-- Make sure we have a unique id for the test, else we can't identify 10 records
set #Count = #Count + 1;
END
Table 2
IF OBJECT_ID('tempdb..#Table1') IS NOT NULL
BEGIN
DROP TABLE #Table1;
END
CREATE TABLE #Table1 (ID INT);
DECLARE #Count int = 0;
WHILE (select count(*) from #Table1) < 5000 BEGIN
INSERT INTO #Table1 (ID)
VALUES (#Count)
-- Make sure we have a unique id for the test, else we can't identify 10 records
set #Count = #Count + 1;
END
/****************** UPDATE ********************/
select count (*) from #Table2 t2 where Exists (select * from #Table1 t1 where t1.ID = t2.ID)
select count (*) from #Table2 where ID = 0
select count (*) from #Table1 where ID = 0
-- While exists an 'un-updated' record continue
WHILE exists (select 1 from #Table2 t2 where Exists (select * from #Table1 t1 where t1.ID = t2.ID) )
BEGIN
-- Update any top 10 'un-updated' records
UPDATE t2
SET ID = 0
FROM #Table2 t2
WHERE ID IN (select top 10 id from #Table2 where Exists (select * from #Table1 t1 where t1.ID = t2.ID) )
END
Your UPDATE statement is referencing the wrong instance on #Table2. You want the following:
UPDATE t2 SET
ID = 0
FROM #Table2 t2
WHERE ID IN (
SELECT TOP 10 ID
-- note this alias is t2a, and is what the `exists` needs to reference
-- not the table being updated (`t2`)
FROM #Table2 t2a
WHERE EXISTS (SELECT 1 FROM #Table1 t1 WHERE t1.ID = t2a.ID)
)
Note: For testing ensure that #Count starts from 1 not 0 else you do still end up with an infinite loop.

Cursor on a trigger

I'm using SQL Server 2008.
I have an after trigger for INSERT, UPDATE and DELETE action defined in the table. My problem is that currently my trigger inserts one record at a time and I need multiple records as for one
SELECT TOP 1 #ParentID FROM ... WHERE ID = #ID
returns multiple unique records.
(See this comment below "-- this subquery returns more than 1 value, so I need to insert in the search Audit table as many ParentIDs as it returns")
I believe I need to use cursor, but I'm not sure where exactly to declare and open cursor.
--CREATE PROCEDURE [dbo].[SP_Auditing]
-- #ID INT, #Code VARCHAR(3), #AuditType VARCHAR(10), #ParentCode VARCHAR(3) = NULL, #ParentID INT = NULL
--AS
--BEGIN
-- INSERT INTO myDB.dbo.Table1 (ID, Code, AuditType, ParentCode, ParentID)
-- VALUES(#ID, #Code, #AuditType, #ParentCode, #ParentID)
--END
GO
CREATE TRIGGER [dbo].[Tr_MyFavouriteTable_UPD_INSERT_DEL] ON [dbo].[MyFavouriteTable] AFTER INSERT, DELETE, UPDATE NOT FOR REPLICATION
AS
BEGIN
DECLARE #ID INT, #Code VARCHAR(3), #AuditType VARCHAR(10), #ParentCode VARCHAR(3), #ParentID INT SET #Code = 'DOC'
IF EXISTS (SELECT 1 FROM inserted) AND
NOT EXISTS (SELECT 1 FROM deleted)
BEGIN
SELECT TOP 1
#ID = ins.ID,
#ParentID = (
SELECT TOP 1 CAST(RIGHT(parentId,LEN(parentId) - LEN(LEFT(parentId,3))) AS INT)
FROM [MyDB].[dbo].[MyFavouriteTable] t WITH (NOLOCK)
INNER JOIN [MyDB2].[dbo].[MyView] v WITH (NOLOCK)
ON t.Id = v.ID
WHERE v.ID = #ID --284
), **-- this subquery returns more than 1 value, so I need to insert in the search Audit table as many ParentIDs as it returns**
#AuditType = 'INSERT' FROM inserted ins
IF #ID IS NOT NULL
AND
#ParentID IS NOT NULL
AND
#ParentCode IS NOT NULL
EXEC [MyDB].[dbo].SP_Auditing] #ID, #Code, #AuditType, #ParentCode, #ParentID
END
-- below is the same logic for UPDATE and DELETE actions...
The stored procedure above simply inserts data into the Audit table.
Never use scalar variables in triggers because insert, update, and delete may affect multiple rows. As to your trigger, try something like this.
CREATE TRIGGER [dbo].[Tr_MyFavouriteTable_UPD_INSERT_DEL]
ON [dbo].[MyFavouriteTable] AFTER INSERT, DELETE, UPDATE
NOT FOR REPLICATION
AS
BEGIN
;with act as (
select isnull(i.id,d.id) id, --either deleted or inserted is not null
case when i.id is not null and d.id is not null then 'update'
when i.id is not null then 'insert'
else 'delete' end auditType
from inserted i full outer join deleted d on i.id = d.id
),
audit_cte as (
SELECT act.id, 'DOC' Code,
CAST(RIGHT(parentId,LEN(parentId) - LEN(LEFT(parentId,3))) AS INT) parentid,
act.auditType, 'parentcode' parentCode
FROM [MyDB].[dbo].[MyFavouriteTable] t WITH (NOLOCK)
INNER JOIN [MyDB2].[dbo].[MyView] v WITH (NOLOCK) ON t.Id = v.ID
inner join act on act.id = t.id
)
insert myDB.dbo.Table1 (ID, Code, AuditType, ParentCode, ParentID)
select id,code,AuditType, ParentCode, ParentID
from audit_cte
where parentCode is not null and parentid is not null
end
Why do you need to get the records one by one? From my understanding you want to keep the log.
IF EXISTS (SELECT 1 FROM inserted) AND
NOT EXISTS (SELECT 1 FROM deleted)
BEGIN
INSERT INTO [Your_Log_Table]
SELECT
ins.ID, [Code],'INSERT',[PrentCode],
(SELECT TOP 1 CAST(RIGHT(parentId,LEN(parentId) -
LEN(LEFT(parentId,3))) AS INT)
FROM [MyDB].[dbo].[MyFavouriteTable] t WITH (NOLOCK)
INNER JOIN [MyDB2].[dbo].[MyView] v WITH (NOLOCK)
ON t.Id = v.ID
WHERE v.ID = ins.ID --284
)
FROM inserted ins
END
See Alex Kudryashev's answer. I needed to tweak a little his logic to sort out duplicate records with the same ParentIDs for the insertion into the Audit table. I added one more cte just below Alex's cte_Audit as follows
CREATE TRIGGER [dbo].[Tr_MyFavouriteTable_UPD_INSERT_DEL]
ON [dbo].[MyFavouriteTable] AFTER INSERT, DELETE, UPDATE
NOT FOR REPLICATION
AS
BEGIN
;with act as (
select isnull(i.id,d.id) id, --either deleted or inserted is not null
case when i.id is not null and d.id is not null then 'update'
when i.id is not null then 'insert'
else 'delete' end auditType
from inserted i full outer join deleted d on i.id = d.id
),
audit_cte as (
SELECT act.id, 'DOC' Code,
CAST(RIGHT(parentId,LEN(parentId) - LEN(LEFT(parentId,3))) AS INT) parentid,
act.auditType, 'parentcode' parentCode
FROM [MyDB].[dbo].[MyFavouriteTable] t WITH (NOLOCK)
INNER JOIN [MyDB2].[dbo].[MyView] v WITH (NOLOCK) ON t.Id = v.ID
inner join act on act.id = t.id
)
insert myDB.dbo.Table1 (ID, Code, AuditType, ParentCode, ParentID)
select id,code,AuditType, ParentCode, ParentID
from audit_cte
where parentCode is not null and parentid is not null
,CTE_dupsCleanup AS (
SELECT DISTINCT
Code,
Id,
AuditType,
ParentCode,
ParentId,
-- ROW_NUMBER() OVER(PARTITION BY ParentId, ParentCode, AuditType ORDER BY ParentId) AS Rn
FROM AUDIT_CTE
WHERE ParentCode IS NOT NULL
AND ParentId IS NOT NULL )
Then using Rn = 1 inserted only unique records into the Auidt table. Like this:
INSERT [ISSearch].[dbo].[SearchAudit] (Code, ID, AuditType, ParentCode, ParentID)
SELECT
Code,
ID,
AuditType,
ParentCode,
ParentId
FROM CTE_dupsCleanup
-- WHERE Rn = 1
END

TSQL: How to ouput records even when where condition doesnt match

I have a temp table with 3 columns "ID","Cost", "MaxCost"..below is my select statement which selects rows given particular ID..
SELECT
t.Cost
t.MaxCost
FROM #temp t
WHERE t.ID = #ID
How do i modify the above query so that even if given ID doesn't exists it still output rows with Cost = 0 & MaxCost = 0
Select both the actual and the default record, and select the first one ordering by their weight.
select top (1)
Cost,
MaxCost
from (
SELECT
t.Cost
t.MaxCost,
1 as takeme
FROM #temp t
WHERE t.ID = #ID
union all
select 0, 0, 0
) foo
order by
foo.takeme desc
declare #table table (cost int);
insert into #table values (2), (2), (3);
declare #findCost int = 1;
select * from #table where cost = #findCost
union all
select 0 as cost from #table where cost = #findCost having count(*) = 0;
set #findCost = 2;
select * from #table where cost = #findCost
union all
select 0 as cost from #table where cost = #findCost having count(*) = 0;

MSSQL Trigger - Updating newly inserted record on INSERT

I wish to make a modification (Set Deleted = 1) to rows being inserted into my table CustomerContact if the SELECT statement returns more than 0.
I have the following, but it remains untested:
CREATE TRIGGER mark_cust_contact_deleted ON CustomerContact
AFTER INSERT AS
BEGIN
DECLARE #numrows INT;
/* Determine if order matches criteria for marking customer contact as DELETED immediately */
SELECT #numrows = COUNT(*)
FROM [Order] o
JOIN OrderMeterDetail om
ON o.OrderID = om.OrderID
WHERE o.WorkTypeID = 3 AND o.WorkActionID = 26 AND o.WorkStageID IN (109, 309, 409)
AND om.MeterDetailTypeID = 1 AND om.MeterLocationID IN (2, 4)
AND o.orderid IN (SELECT OrderID FROM INSERTED);
/* If the order matches the criteria, mark the customer contact as deleted */
IF (#numrows >= 1)
UPDATE CustomerContact
SET Deleted = 1
WHERE CustomerContactID IN (SELECT CustomerContactID FROM INSERTED);
END
Within my IF statement, I am using FROM INSERTED, assuming that this will return the newly inserted id for the record that was created by the insert.
I have two questions about this statement:
Will this part of the statement perform an UPDATE just the record
that was just inserted into CustomerContact?
UPDATE CustomerContact
SET Deleted = 1
WHERE CustomerContactID IN (SELECT CustomerContactID FROM INSERTED);
Is this the way that would be deemed correct to make a change to a row that has just been inserted based on the result of a SELECT statement?
CustomerContactID is an auto-incrementing primary key column.
You say "Just the record that was inserted". Inserted can contain more than one record. If there is only one, then your trigger will function as you expect. But if there is more than one, it won't.
I would rewrite your logic into a single update statement along the lines of...
Update CustomerContact
Set Deleted = 1
From CustomerContact
inner join inserted on CustomerContact.CustomerContactID = inserted.CustomerContactID
inner join orders on inserted.OrderID = orders.OrderID
where
-- some criteria.
CREATE TRIGGER mark_cust_contact_deleted ON CustomerContact
AFTER INSERT AS
BEGIN
DECLARE #numrows INT;
/* Determine if order matches criteria for marking customer contact as DELETED immediately */
-- Get all the records into a temp table
SELECT * INTO #Temp
FROM inserted
Declare #ID int;
SELECT #numrows = COUNT(*)
FROM [Order] o
JOIN OrderMeterDetail om
ON o.OrderID = om.OrderID
WHERE o.WorkTypeID = 3 AND o.WorkActionID = 26 AND o.WorkStageID IN (109, 309, 409)
AND om.MeterDetailTypeID = 1 AND om.MeterLocationID IN (2, 4)
AND o.orderid IN (SELECT OrderID FROM #Temp);
IF (#numrows >= 1)
BEGIN
WHILE EXISTS (SELECT TOP 1 * FROM #Temp)
BEGIN
SELECT TOP 1 #ID = ID FROM #Temp
/* If the order matches the criteria, mark the customer contact as deleted */
UPDATE CustomerContact
SET Deleted = 1
WHERE CustomerContactID IN (SELECT CustomerContactID FROM #Temp WHERE ID = #ID);
DELETE FROM #Temp WHERE ID = #ID
END
END
DROP TABLE #Temp
END
I think you can do something like this, tweak the code to futher suit for needs, hope this will help.
Here is the final solution that I used to solve this issue:
CREATE TRIGGER mark_cust_contact_deleted ON CustomerContact
AFTER INSERT AS
BEGIN
UPDATE CustomerContact
SET Deleted = 1
FROM CustomerContact cc
JOIN inserted i
ON cc.CustomerContactID = i.CustomerContactID
JOIN [Order] o
ON i.OrderID = o.OrderID
JOIN OrderMeterDetail om
ON i.OrderID = om.OrderID
WHERE o.WorkTypeID = 3 AND o.WorkActionID = 26 AND o.WorkStageID IN (109, 309, 409)
AND om.MeterDetailTypeID = 1 AND om.MeterLocationID IN (2, 4)
END

Resources