Dead Lock occur when foreign key exists on table - sql-server

There are two table exist in DB, Audit and AuditField, following is the Create table code:
-- Primary key: ID
CREATE TABLE [dbo].[Audit](
[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[TypeName] [varchar](50) NOT NULL
)
GO
-- Primary key: ID
CREATE TABLE [dbo].[AuditField](
[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[AuditID] [int] NOT NULL,
[Field1] [varchar](50) NOT NULL
)
GO
-- Set foreign key on AuditField table
ALTER TABLE [dbo].[AuditField]
ADD CONSTRAINT [FK_AuditFiled_Audit] FOREIGN KEY([AuditID])
REFERENCES [dbo].[Audit] ([ID])
GO
Then I prepared some test data:
DECLARE #audit TABLE
(
ID int not null,
TypeName varchar(50)
)
DECLARE #auditField TABLE
(
AuditID int not null,
Field1 varchar(50)
)
-- ADD TEST DATA
DECLARE #i int = 1
DECLARE #rowCount int = 500
WHILE #i<=#rowCount
BEGIN
INSERT INTO #audit
VALUES(#i, 'SomeTypeName')
INSERT INTO #auditField
(AuditID,Field1)
VALUES(#i,'SomeThing')
SET #i += 1
END
Finally, i run following transaction to insert test data to these two table:
begin transaction
INSERT INTO dbo.Audit
SELECT TypeName
FROM #audit
ORDER BY ID
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
INSERT INTO dbo.AuditField
SELECT AuditID+#offSet AS AuditID, Field1
FROM #auditField
ORDER BY AuditID
commit transaction
When this transaction run concurrent, dead lock occur, one process are failed, the other got an error:
Msg 547, Level 16, State 0, Line 40 The INSERT statement conflicted
with the FOREIGN KEY constraint "FK_AuditFiled_Audit". The conflict
occurred in database "MyDB", table "dbo.Audit", column 'ID'.
There is no trigger on Audit and AuditField table.
Sorry for the format of the code, I really need an answer why this dead lock occur, thanks.
One thing should be clear, the data of AuditField table comes from #auditField, As #Bogdan answer I rewrite like this:
begin transaction
INSERT INTO dbo.Audit
OUTPUT inserted.ID INTO #temp
SELECT TypeName
FROM #audit
INSERT INTO #idMapping
SELECT ROW_NUMBER() OVER(ORDER BY ID) AS RowNumber, ID
FROM #temp
INSERT INTO dbo.AuditField
SELECT m.ID AS AuditID, Field1
FROM #auditField af
INNER JOIN #idMapping m ON af.AuditID = m.RowNumber
commit transaction

This is and Read - Write deadlock:
As you can see, every transaction has successfully acquired an [e]X[clusive] lock and it requests a S[hared] lock. The question is why a transaction try to read rows X locked by another transaction.
And the answer is bellow:
1) Following piece of source code
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
assumes that IDENTITY values generated by every
INSERT INTO dbo.Audit
SELECT TypeName
FROM ...
statement are continues. This is completely wrong as you can see in the following picture:
This means that at some point in time, a transaction could successfully get X locks on inserted rows and then
1) Because inserted rows into Audit aren't continuous and
2) Because of
declare #lastIdentity int = ##identity
declare #offSet int = #lastIdentity - #rowCount
INSERT INTO dbo.AuditField
SELECT AuditID+#offSet AS AuditID, Field1 ...
this last INSERT tries to insert into dbo.AuditField, AuditID values that belong to another transaction and this requires FK validation and, also, means that SQL Server needs to read rows from dbo.Audit. For this S[hared] locks are needed.
To be clear: the root cause of this deadlock is not the FK constraint. The real problem is that source code.
Solution: I would rewrite thus:
begin transaction
INSERT INTO dbo.Audit
OUTPUT inserted.ID, inserted.TypeName INTO #audit (ID, TypeName)
SELECT TypeName
FROM #audit
-- ORDER BY ID -- Isn't necessary
... do something (ex. DELETE) with rows from #audit
INSERT INTO dbo.AuditField (AuditID, ...)
SELECT x.ID, ...
FROM #audit x
-- ORDER BY AuditID
/* or
INSERT INTO dbo.AuditField (AuditID, Field1, ....)
SELECT y.ID, y.ColumnName, ...
FROM (
SELECT x.ID, ...
FROM #audit x
UNPIVOT( ColumnValue FOR ColumnName IN ([TypeName], ...) )
) y
WHERE y.....
*/
commit transaction -- Isn't necessary

You are trying to insert an invalid foreign key value into dbo.AuditField:
SELECT AuditID+#offSet AS AuditID, Field1
Why the #offset? You won't necessarily have an AuditId with that value in the dbo.Audit table.

Related

I would like to sum all of the data in a specific table column, if the appropriate ID of that table exists in another one

I would like to SUM a value called amount from table 1, the considered, to be summed, values should only be the ones presenting in table 2. Meaning that, for the amount of row 1 in table 1 to be considered in the sum. the ID of that row 1 should be present in table 2.
Thanks,
This might be the answer but you really should have put some example tables in your example. I fancied helping as have 10 mins, this example you can run.
You can see that table 1 is referenced twice from Table2 and 3 just the once ,so the result ignores multiple occurrences, hence the WHERE EXISTS syntax.
This sums up all the numbers in Table1 that are referenced in Table2.
BEGIN TRANSACTION
BEGIN TRY
CREATE TABLE #Table1 (
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Number] DECIMAL NOT NULL
)
INSERT INTO #Table1 ([Number])
VALUES('1'),
('1'),
('1'),
('1')
SELECT * FROM #Table1
CREATE TABLE #Table2 (
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
Table1Id INT NOT NULL,
CONSTRAINT FK_Table2_Table1 FOREIGN KEY (Table1Id) REFERENCES #Table1 (Id)
)
INSERT INTO #Table2 ([Table1Id])
VALUES('1'),
('1'),
('3')
SELECT * FROM #Table2
SELECT SUM(T1.Number) AS SummedNumbersThatAreReferencedByTable2
FROM #Table1 AS T1
WHERE EXISTS(
SELECT TOP 1 *
FROM #Table2 AS T2
WHERE T2.Table1Id = T1.Id
)
ROLLBACK TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Rolling back changes, there was an error!!'
ROLLBACK TRANSACTION
DECLARE #Msg NVARCHAR(MAX)
SELECT #Msg=ERROR_MESSAGE()
RAISERROR('Error Occured: %s', 20, 101,#msg) WITH LOG
END CATCH
If this is the answer then please mark it as so, cheers

Random Primary Key missing in SQL Server selection

I am facing a problem when I query master table (having ~700 Million records and high transactional table) to look for newly inserted records. My aim is to get all the newly created IDs from the #IDs temp table (Min and Max records) and dump it in another child table. But random IDs are missing in the child table.
Setup:
We have a primary and secondary server (SQL Server 2016) and they are in sync mode.
Tables:
CREATE TABLE tblMaster
(
ID BIGINT IDENTITY(1,1) NOT NULL,
EmployeeID INT NOT NULL
)
CREATE TABLE tblChild
(
ChildID IDENTITY(1,1),
ID BIGINT NOT NULL,
TransactionDate Datetime NOT NULL
)
tblChild.ID references tblMaster.ID.
Stored procedure:
DECLARE #MaxID BIGINT
SELECT #MaxID = MAX(ID) FROM tblChild WITH(NOLOCK)
SET #MaxID = ISNULL(#MaxID, 0)
DROP TABLE IF EXISTS #IDS
SELECT ID
INTO #IDS
FROM tblMaster WITH(NOLOCK)
WHERE ID > #MaxID
--25k RECORDS BATCH INSERT INTO tblChild - MAINLY TAKE CARE NEWLY inserted records
STARTIDS:
IF EXISTS (SELECT TOP 1 * FROM #IDS)
BEGIN
DROP TABLE IF EXISTS #TOPIDS
SELECT TOP 25000 ID INTO #TOPIDS
FROM #IDS
ORDER BY ID ASC
INSERT INTO tblChild (ID, CreatedBy, CreatedDate)
SELECT ID, SYSTEM_USER, GETDATE()
FROM #TOPIDS
DELETE AA
FROM #IDS AA
INNER JOIN #TOPIDS BB ON AA.ID = BB.ID
GOTO STARTIDS
END
Please help where it's going wrong.

Throw error on invalid insertion in SQL Server

I have a SQL Server 2012 database with two tables:
CREATE TABLE Products
(
Id INT IDENTITY(1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product
PRIMARY KEY CLUSTERED (Id ASC)
);
CREATE TABLE BlockedProductCodes
(
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes
PRIMARY KEY CLUSTERED (Code ASC)
);
I want to be able to prevent products being inserted into the Products table if their product code exists in the BlockedProductCodes table.
The only way I could think of doing this was with a BEFORE INSERT trigger:
CREATE TRIGGER trg_Products_BEFORE_INSERT
ON Products
INSTEAD OF INSERT AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT Code
FROM BlockedProductCodes BPC
INNER JOIN inserted I ON BPC.Code = I.Code)
BEGIN
RAISERROR('The product has been blocked!', 16, 1);
END
ELSE
BEGIN
INSERT Product (Id, Code, Name)
SELECT Id, Code, Name
FROM INSERTED
END
SET NOCOUNT OFF;
END
But this caused an error with the identity column:
Cannot insert explicit value for identity column in table 'Products' when IDENTITY_INSERT is set to OFF
Can anyone suggest a way to fix this or a better approach?
Please note, this check is also made at the application level, but I want enforce that at the data table level.
Thanks.
Update: using check constraint
I have tried the following that seems to work..
CREATE FUNCTION dbo.IsCodeBlocked
(
#code nvarchar(50)
)
RETURNS BIT
WITH SCHEMABINDING
AS
BEGIN
DECLARE #ret bit
IF (#Code IN (SELECT Code FROM dbo.BlockedProductCodes))
SET #ret = 1
ELSE
SET #ret = 0
RETURN #ret
END
GO
ALTER TABLE Products
ADD CONSTRAINT CheckValidCode
CHECK (dbo.IsCodeBlocked(Code) = 0);
GO
insert Products (Code, Name) values ('xyz', 'Test #1')
go
insert Products (Code, Name) values ('abc', 'Test #2')
-- Fails with "The INSERT statement conflicted with the
-- CHECK constraint 'CheckValidCode'."
go
I am not sure if it is particularly 'safe' or performant. I will also test out the indexed view approach suggested by Damien.
One way you can implement this is by abusing an indexed view:
CREATE TABLE dbo.Products (
Id INT IDENTITY (1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product PRIMARY KEY CLUSTERED (Id ASC)
);
GO
CREATE TABLE dbo.BlockedProductCodes (
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes PRIMARY KEY CLUSTERED (Code ASC)
);
GO
CREATE TABLE dbo.Two (
N int not null,
constraint CK_Two_N CHECK (N > 0 and N < 3),
constraint PK_Two PRIMARY KEY (N)
)
GO
INSERT INTO dbo.Two(N) values (1),(2)
GO
create view dbo.DRI_NoBlockedCodes
with schemabinding
as
select
1 as Row
from
dbo.Products p
inner join
dbo.BlockedProductCodes bpc
on
p.Code = bpc.Code
inner join
dbo.Two t
on
1=1
GO
CREATE UNIQUE CLUSTERED INDEX IX_DRI_NoBlockedCodes on dbo.DRI_NoBlockedCodes (Row)
And now we attempt to insert:
INSERT INTO dbo.BlockedProductCodes (Code,ReasonCode) values ('abc',10)
GO
INSERT INTO dbo.Products (Code,Name) values ('abc','def')
And we get:
Msg 2601, Level 14, State 1, Line 42
Cannot insert duplicate key row in object 'dbo.DRI_NoBlockedCodes' with unique index 'IX_DRI_NoBlockedCodes'. The duplicate key value is (1).
The statement has been terminated.
So if that error message is acceptable to you, this could be one way to go. Note, that if you have a numbers table, you can use that instead of my dummy Two table.
The trick here is to construct the view in such a way so that, if there's ever a match between the Products and BlockedProductCodes tables, we produce a multi-row result set. But we've also ensured that all rows have a single constant column value and there's a unique index on the result - so the error is generated.
Note that I've used my convention of prefixing the table name with DRI_ when it exists solely to enforce an integrity constraint - I don't intend that anyone will ever query this view (indeed, as shown above, this view must always in fact be empty)

How to prevent a single date overlap in sql server 2012

I have a table EmployeeLeaves having columns:
EmployeeID int,
LeaveTypeID int,
LeaveDate datetime2,
IsHalfDay bit,
DateCreated datetime2,
Createdby varchar
I request you to help me out with a trigger that prevent same date for the column "LeaveDate". There are no date ranges like "Todate" and "FromDate" in the table.
Thanks in advance.
Please refer the sample demo,
CREATE TABLE EmployeeLeaves
(
EmployeeID INT,
LeaveTypeID INT,
LeaveDate DATETIME2,
IsHalfDay BIT,
DateCreated DATETIME2,
Createdby VARCHAR(50)
)
insert into EmployeeLeaves
values (1,1,'2016-01-01',0,getdate(),'Admin'),
(2,1,'2016-01-01',0,getdate(),'Admin'),
(3,1,'2016-01-01',0,getdate(),'Admin'),
(4,1,'2016-01-01',0,getdate(),'Admin'),
(5,1,'2016-01-01',0,getdate(),'Admin')
SELECT *
FROM EmployeeLeaves
METHOD-1 using Unique Constraint
--Introduce the unique constraint
ALTER TABLE EmployeeLeaves
ADD CONSTRAINT uq_employeeid_leavedate UNIQUE (EmployeeId, LeaveDate)
--Try to create overlap
insert into EmployeeLeaves
values (1,1,'2016-01-01',0,getdate(),'Admin')
--You will get the following error
--Msg 2627, Level 14, State 1, Line 1
--Violation of UNIQUE KEY constraint 'uq_employeeid_leavedate'. Cannot insert duplicate key in object 'dbo.EmployeeLeaves'. The duplicate key value is (1, 2016-01-01 00:00:00.0000000).
--The statement has been terminated.
--Insert proper date
insert into EmployeeLeaves
values (1,1,'2016-01-02',0,getdate(),'Admin')
--Check the result
SELECT *
FROM EmployeeLeaves
METHOD-2 using Instead of trigger
ALTER TRIGGER Trigger_Test
ON EmployeeLeaves
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS(SELECT TOP 1 1
FROM inserted I
INNER JOIN EmployeeLeaves e
ON i.EmployeeID = e.EmployeeID
AND i.LeaveDate = e.LeaveDate)
BEGIN
RAISERROR (N'Overlapping range.',16,1);
ROLLBACK TRANSACTION;
END
ELSE
BEGIN
INSERT INTO EmployeeLeaves
SELECT *
FROM inserted
END;
END
You can add a unique constraint on leave date field or you can check and raise exception using
if exist( select top 1 EmployeeID from table where EmployeeID = #emploeeid and
datediff(day,leavedate,#applydate)=0 and leavetypeid= #leavetypeid)
begin raise_error('leave already exist',1,1) end

Inserting batch of rows into two tables in SQL Server 2008

I have a requirement to insert multiple rows into table1 and at the same time insert a row into table2 with a pkID from table1 and a value that comes from a SP parameter.
I created a stored procedure that performs a batch insert with a table valued parameter which contains the rows to be inserted into table1. But I have a problem with inserting the row into table2 with the corresponding Id (identity) from table1, along with parameter value that I have passed.
Is there anyone who implemented this, or what is the good solution for this?
CREATE PROCEDURE [dbo].[oSP_TV_Insert]
#uID int
,#IsActive int
,#Type int -- i need to insert this in table 2
,#dTableGroup table1 READONLY -- this one is a table valued
AS
DECLARE #SQL varchar(2000)
DECLARE #table1Id int
BEGIN
INSERT INTO dbo.table1
(uID
,Name
,Contact
,Address
,City
,State
,Zip
,Phone
,Active)
SELECT
#uID
,Name
,Contact
,Address
,City
,State
,Zip
,Phone
,Active
,#G_Active
FROM #dTableGroup
--the above query will perform batch insert using the records from dTableGroup which is table valued
SET #table1ID = SCOPE_IDENTITY()
-- this below will perform inserting records to table2 with every Id inserted in table1.
Insert into table2(#table1ID , #type)
You need to temporarily store the inserted identity values and then create a second INSERT statement - using the OUTPUT clause.
Something like:
-- declare table variable to hold the ID's that are being inserted
DECLARE #InsertedIDs TABLE (ID INT)
-- insert values into table1 - output the inserted ID's into #InsertedIDs
INSERT INTO dbo.table1(ID, Name, Contact, Address, City, State, Zip, Phone, Active)
OUTPUT INSERTED.ID INTO #InsertedIDs
SELECT
#ID, Name, Contact, Address, City, State, Zip, Phone, Active, #G_Active
FROM #dTableGroup
and then you can have your second INSERT statement:
INSERT INTO dbo.table2(Table1ID, Type)
SELECT ID, #type FROM #InsertedIDs
See the MSDN docs on the OUTPUT clause for more details on what you can do with the OUTPUT clause - one of the most underused and most "unknown" features of SQL Server these days!
Another approach using OUTPUT clause and only one statement for inserting data in both destination tables:
--Parameters
DECLARE #TableGroup TABLE
(
Name NVARCHAR(100) NOT NULL
,Phone VARCHAR(10) NOT NULL
);
DECLARE #Type INT;
--End Of parameters
--Destination tables
DECLARE #FirstDestinationTable TABLE
(
FirstDestinationTableID INT IDENTITY(1,1) PRIMARY KEY
,Name NVARCHAR(100) NOT NULL
,Phone VARCHAR(10) NOT NULL
);
DECLARE #SecondDestinationTable TABLE
(
SecondDestinationTable INT IDENTITY(2,2) PRIMARY KEY
,FirstDestinationTableID INT NOT NULL
,[Type] INT NOT NULL
,CHECK([Type] > 0)
);
--End of destination tables
--Test1
--initialization
INSERT #TableGroup
VALUES ('Bogdan SAHLEAN', '0721200300')
,('Ion Ionescu', '0211002003')
,('Vasile Vasilescu', '0745600800');
SET #Type = 9;
--execution
INSERT #SecondDestinationTable (FirstDestinationTableID, [Type])
SELECT FirstINS.FirstDestinationTableID, #Type
FROM
(
INSERT #FirstDestinationTable (Name, Phone)
OUTPUT inserted.FirstDestinationTableID
SELECT tg.Name, tg.Phone
FROM #TableGroup tg
) FirstINS
--check records
SELECT *
FROM #FirstDestinationTable;
SELECT *
FROM #SecondDestinationTable;
--End of test1
--Test2
--initialization
DELETE #TableGroup;
DELETE #FirstDestinationTable;
DELETE #SecondDestinationTable;
INSERT #TableGroup
VALUES ('Ion Ionescu', '0210000000')
,('Vasile Vasilescu', '0745000000');
SET #Type = 0; --Wrong value
--execution
INSERT #SecondDestinationTable (FirstDestinationTableID, [Type])
SELECT FirstINS.FirstDestinationTableID, #Type
FROM
(
INSERT #FirstDestinationTable (Name, Phone)
OUTPUT inserted.FirstDestinationTableID
SELECT tg.Name, tg.Phone
FROM #TableGroup tg
) FirstINS
--check records
DECLARE #rc1 INT, #rc2 INT;
SELECT *
FROM #FirstDestinationTable;
SET #rc1 = ##ROWCOUNT;
SELECT *
FROM #SecondDestinationTable;
SET #rc2 = ##ROWCOUNT;
RAISERROR('[Test2 results] #FirstDestinationTable: %d rows; ##SecondDestinationTable: %d rows;',1,1,#rc1,#rc2);
--End of test1
Since you need all inserted identity values, look at the output clause of the insert statement: http://msdn.microsoft.com/en-us/library/ms177564.aspx

Resources