Stored procedure structure and concurrency issues - sql

Someone wrote the following stored procedure in SQL Server 2008. I think it's just not one the best ways to achieve what he needed to achieve.
As you can see that we need to update MyTable under some conditions but not under the other. And whenever there is a condition that lets a user update MyTable we don't want multiple users to update MyTable concurrently. The thing is the way this procedure is written all the variables are set using select statements and then Begin Transaction starts.
But given the structure of this procedure is it better to execute an sp_getapplock procedure to allow just one user to do the update (that might work well with this procedure given its structure but I read all over that sp_getapplock can lead to deadlocks) and the other options is to do a serializable READ_COMMITED.
But in case of serializable wouldn't this stored procedure allow one of the concurrent user (out of say 2) still select the old values of col1, col2, col3 and col4 and populate variables like #onGoingSession, #timeDiff etc with those old values when the other done updating and released the row (through serializable).Since after the transaction completion it's the turn of second user. And I do wanna block other users from reading the row that is being modified.
Shouldn't we move the begin transaction up right after declaring the variables and the do a serializable after it. So that no concurrent users should read old values and populate the variables while one of the users (apparently who already acquired the row lock) is updating the locked row but hasn't committed as yet.
USE [myDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[MyTableProc]
(#UserId char(20))
AS
BEGIN
DECLARE #error varchar(200)
DECLARE #warning varchar(300)
DECLARE #timeDiff smallint
DECLARE #onGoingSession bit = 1
DECLARE #userName char(20)
DECLARE #counterOfResync tinyint
SET #counterOfResync = (SELECT [Col1] FROM MyTable)
SET #userName = (SELECT [Col2] FROM MyTable)
SET #onGoingSession = (SELECT [Col3] FROM MyTable)
SET #timeDiff = (SELECT DATEDIFF(MINUTE, Col4, CURRENT_TIMESTAMP) from MyTable)
BEGIN TRANSACTION
IF(#onGoingSession = 1)
BEGIN
IF(#timeDiff >= 360)
BEGIN
UPDATE MyTable
SET UserId = #UserId, Col4 = CURRENT_TIMESTAMP
IF(##ERROR = 0)
BEGIN
SET #warning = RTRIM('An unfinsihed session for ' +
LTRIM(RTRIM(#userName)) + ' is going on for the past ' +
LTRIM(RTRIM(#timeDiff)) + ' minutes but updates from ' + LTRIM(RTRIM(#UserId)) + ' are successful')
COMMIT
RAISERROR(#warning,7, 1)
RETURN
END
ELSE
BEGIN
ROLLBACK
RETURN ##ERROR
END
END
ELSE
BEGIN
SET #error = RTRIM('A session of updates for '+ LTRIM(RTRIM(#userName))+ ' is already in progress concurrent updates are not allowed')
IF(##ERROR = 0)
BEGIN
COMMIT
RAISERROR(#error, 8, 1)
RETURN
END
ELSE
BEGIN
ROLLBACK
RETURN ##ERROR
END
END
END
ELSE IF(#onGoingSession = 0 AND #counterOfResync = 0)
BEGIN
UPDATE MyTable
SET UserId = #UserId, Col3 = 1, Col4 = CURRENT_TIMESTAMP
IF(##ERROR =0)
BEGIN
COMMIT
RETURN
END
ELSE
BEGIN
ROLLBACK
RETURN ##ERROR
END
END
ELSE IF(#onGoingSession = 0 AND #counterOfResync > 0 AND #timeDiff >= 5)
BEGIN
UPDATE MyTable
SET Col3 = 1, CountOfResync = 0, UserId = #UserId, Col4 =
CURRENT_TIMESTAMP
IF(##ERROR = 0)
BEGIN
COMMIT
RETURN
END
ELSE
BEGIN
ROLLBACK
RETURN ##ERROR
END
END
ELSE
BEGIN
SET #error = RTRIM('A server resync session already in progress, updates can''t be made at the same time')
IF(##ERROR = 0)
BEGIN
COMMIT
RAISERROR(#error, 9, 1)
RETURN
END
ELSE
BEGIN
ROLLBACK
RETURN ##ERROR
END
END
RETURN ##ERROR
END

Something like the following would reduce the transaction to a single update statement with the logic to update individual columns handled by case expressions. Post processing handles any errors and generating the curious variety of results expected.
create procedure dbo.MyTableProc( #UserId as Char(20) )
as
begin
declare #CounterOfResync as TinyInt;
declare #Error as Int;
declare #ErrorMessage as VarChar(200);
declare #OngoingSession as Bit = 1;
declare #PriorUsername as Char(20);
declare #TimeDiff as SmallInt;
declare #WarningMessage as VarChar(300);
update MyTable
set
#CounterOfResync = Col1,
#PriorUsername = UserId,
#OngoingSession = Col3,
#TimeDiff = DateDiff( minute, Col4, Current_Timestamp ),
UserId = #UserId,
Col2 = case when Col3 = 0 and Col1 > 0 and DateDiff( minute, Col4, Current_Timestamp ) >= 5 then 1 else Col2 end,
Col3 = case when Col1 = 0 and Col3 = 0 then 1 else Col3 end,
Col4 = Current_Timestamp
set #Error = ##Error;
if ( #CounterOfResync = 1 )
if ( #TimeDiff >= 360 )
begin
if ( #Error = 0 )
begin
set #WarningMessage = 'An unfinished session for user ' + #PriorUsername + ' is going on for the past ' +
Cast( #TimeDiff as VarChar(10) ) + ' minutes but updates from ' + #UserId + ' are successful';
RaIsError( #WarningMessage, 7, 1 );
end
else
return #Error;
end
else
begin
if ( #Error = 0 )
begin
set #ErrorMessage = 'A session of updates for '+ #PriorUsername + ' is already in progress concurrent updates are not allowed';
RaIsError( #ErrorMessage, 8, 1 );
end
else
return #Error;
end
else
if ( #OngoingSession = 0 and #CounterOfResync = 0 )
return #Error
else
-- ...
Apologies for any errors in trying to wade through the existing code and translate it into something stranger. My intent is to provide a (mis)direction that you may choose to follow, not the completed code.

Related

SQL Server stored procedure running in infinite loop

I am running a stored procedure which is running infinitely.
I have used a while loop that seems to be running without ever ending.
CREATE PROCEDURE ABC
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Id INT;
DECLARE #iter INT = 1;
DECLARE #iterMax INT;
DECLARE #psubject VARCHAR(100);
DECLARE #pbody NVARCHAR(MAX);
DECLARE #pSendTo NVARCHAR(MAX);
DECLARE #pProfile VARCHAR(MAX);
IF OBJECT_ID('tempdB..#temp') IS NOT NULL
DROP TABLE #temp;
SET #pProfile = 'Test';
IF ((SELECT COUNT(*)
FROM [Table_A] R
JOIN [Table_B] T ON R.Id = T.r_Id
WHERE R.[Date] <= (DATEADD(DAY, -1, GETDATE()))
AND T.[Sent_Flag] IS NULL) >= 1)
BEGIN
SELECT IDENTITY(int, 1, 1) AS RecId,
[r_id] * 1 AS Id
INTO #temp
FROM [Table_A] R
JOIN [Table_B] T ON R.Id = T.r_Id
WHERE R.[Date] <= (DATEADD(DAY, -1, GETDATE()))
AND T.[Sent_Flag] IS NULL;
BEGIN
SET #iterMax = (SELECT COUNT(*)FROM #temp);
WHILE (#iter <= #iterMax)
BEGIN
SET #psubject = 'HIIII'; /*this is in HTML example */
SET #pbody = 'You got one email';
IF ((SELECT COUNT(*)
FROM [Table_B]
WHERE R_Id = (SELECT Id FROM #temp WHERE RecId = #iter)
AND [Mail1_Flag] = 'Y'
AND [Mail2_Flag] = 'Y') = 1)
BEGIN
IF ((SELECT COUNT(*)
FROM [Table_A] R
JOIN [Table_B] T ON R.Id = T.r_Id
WHERE R_Id = (SELECT Id FROM #temp WHERE RecId = #iter)
AND R.[Date] <= (DATEADD(DAY, -1, GETDATE()))
AND T.[Sent_Flag] IS NULL) = 1)
BEGIN
SET #pSendTo = N'ABC#gmail.com';
EXEC msdb.dbo.sp_send_dbmail #profile_name = #pProfile,
#body = #pbody,
#subject = #psubject,
#recipients = #pSendTo,
#body_format = 'HTML';
END;
UPDATE [Table_B]
SET [Sent_Flag] = 'Y'
WHERE [Id] IN (SELECT Id FROM #temp WHERE RecId = #iter);
END;
END;
SET #iter = #iter + 1;
END;
END;
IF OBJECT_ID('tempd..#temp') IS NOT NULL
DROP TABLE #temp;
END;
This program is checking that if date is more than 24 hours then it will send a mail correspondingly. I am able to trigger a mail. But I am getting multiple mails. Like the loop is running infinitely and getting same mail multiple times and the sent_Flag column is initial NULL and after a mail is sent it sholud update to 'Y' but it is also not updating to 'Y' after mail is triggered.
Please help to resolve this issue. Thank you
You are not incrementing the counter inside the loop:
UPDATE [Table_B]
SET [Sent_Flag] = 'Y'
WHERE [Id] IN (SELECT Id FROM #temp WHERE RecId = #iter);
END; --this is the END of the first IF BEGIN
END; --this is the END of the WHILE BEGIN
SET #iter = #iter + 1; --and here you update the counter, which will never be reached
If you don't increment the counter inside the loop, the loop will run infinitely, since the loop condition will always be true.

How to avoid concurrent read access issue in SQL Server?

I am getting a date value from a tblinfo table. If the date is null, then I will insert into table, else I will update the row in the table.
I am facing the issue that 2 sessions accessing the table simultaneously with same information, both are getting Null, so both are trying to insert records into same table with same primary key value. We are getting the primary key constraints issue. How to stop this?
begin tran
set #date=(select date from tblinfo where id=#id)
if(#date is null)
--insert
else
--update
commit tran
Using Locking
declare #Date DateTime
, #RowId bigint = 10
Begin Tran
select #Date = SomeDate
from tblInfo
with (RowLock) --this is the important point; during the transaction, this row will be locked, so other statements won't be able to read it
where Id = #RowId
if (#Date is Null)
begin
insert SomeTable (TblInfoId, Date)
values (#RowId, GetUtcDate())
end
else
begin
update SomeTable
set Date = #Date
where tblInfoId = #RowId
end
if ##TranCount > 0 Commit Tran
Technique Avoiding Locking
If preventing reads from occurring is an issue for some reason, the below is a lockless approach; but requires an extra field in your database.
declare #Date DateTime
, #RowId bigint = 10
, #DataAvailable bit
Begin Tran
update tblInfo
set session_lock = ##SPID
where Id = #RowId
and session_lock is null
select #Date = SomeDate
, #DataAvailable = 1
from tblInfo
where Id = #RowId
and session_lock = ##SPID
if (#DataAvailable = 1)
begin
if (#Date is Null)
begin
insert SomeTable (TblInfoId, Date)
values (#RowId, GetUtcDate())
end
else
begin
update SomeTable
set Date = #Date
where tblInfoId = #RowId
end
update tblInfo
set session_lock = null
where Id = #RowId
and session_lock = ##SPID
end
--optionally do something if the row was already "locked"
--else
--begin
--...
--end
if ##TranCount > 0 Commit Tran
Right after "begin tran", update your table like below script
begin tran
-- this update will lock the table so no other process can read data
update tblInfo with (tablock)
set date = date
---- do what ever you need to do here
set #date=(select * from tblinfo)
if(#date is null)
--insert
else
--update
commit tran
this will cause SQL Server lock the table and the second transaction will wait reading the data until the first process finish.
Relying on lock hints to get the desired outcome will perhaps prevent you from attempting to insert the value twice, but it won't prevent errors from happening. You'll just get an 'Unable to acquire lock' or deadlock error instead.
You should put a mutex in your code, in the minimally sized critical section. Using sp_getApplock, you can get a lock on a section of code with a wait period that you specify (subsequent threads will wait for the lock to clear and then continue). Here's some sample code:
declare #LockResource nvarchar(100) = 'VALUE_INSERT'
begin transaction
-- Fetch the lock:
EXEC #res = sp_getapplock
#Resource = #LockResource,
#LockMode = 'Exclusive',
#LockOwner = 'Transaction',
#LockTimeout = 10000,
#DbPrincipal = 'public'
if #res not in (0, 1)
begin
set #msg = 'Unable to acquire Lock for ' + #LockResource
raiserror (#msg , 16, 1)
rollback transaction
return
end
else
begin
begin try
---------------------------------------------
-- Fetch value if it exists, insert if not.
-- Both need to happen here.
---------------------------------------------
end try
begin catch
select #msg = 'An error occurred: ' + ERROR_MESSAGE()
-- Release the lock.
EXEC #res = sp_releaseapplock
#Resource = #LockResource,
#DbPrincipal = 'public',
#LockOwner = 'Transaction'
rollback transaction
raiserror(#msg, 11, 1)
goto cleanup
end catch
-- Release the lock.
EXEC #res = sp_releaseapplock
#Resource = #LockResource,
#DbPrincipal = 'public',
#LockOwner = 'Transaction'
end
commit transaction

Creating an Instead of insert Trigger SQL

I am a DBA with my company. I am trying to create trigger that will check any insert statement for duplicates first then if none allow the original insert. Not even sure this can be done. The insert statements may be written by various users so the statements will never be the same. All I have found so far is the check for duplicates but the insert statement is then hard coded in the trigger. My plan is also to check update as well, but it is not important right now.
Here is my current code.
ALTER TRIGGER [dbo].[BlockDuplicatesOnTable]
ON [dbo].[blockduplicates]
Instead of INSERT, Update
AS
BEGIN
SET NOCOUNT ON;
Declare #ProviderAcctNumber nvarchar(50)
Declare #Referredby int
Declare #Action as char(1)
Declare #Count as int
Set #Action = 'I'
Select #Count = Count(*) from DELETED
IF #Count > 0
Begin
Set #Action = 'D'
Select #Count = count(*) from INSERTED
IF #Count > 0
Set #Action = 'U'
IF #Action = 'I'
Begin
IF not exists (Select 1 from inserted as i
inner join dbo.blockduplicates as b
on i.ProviderAcctNumber = b.ProviderAcctNumber
and i.Referredby = b.Referredby)
Begin
--execute original insert
End
Else
Begin
Print 'Duplicate insert'
Raiserror ('Duplicate Entry for Insert',16,1);
Return
End
End
Else IF #Action = 'U'
Begin
Select #ProviderAcctNumber = ProviderAcctNumber, #Referredby = Referredby from inserted
IF Not exists (Select 1 from deleted where ProviderAcctNumber = #ProviderAcctNumber and Referredby = #Referredby)
Begin
Print 'Update Statement is True';
End
Else
Begin
Print 'duplicate'
Raiserror ('Duplicate Entry for Update',16,1);
Return
End
End
End
End;

Stored Procedure does not insert data

I am going nuts with a SQL Server stored procedure, it is supposed to register an user into the database (If the user does not already exists). But, after successfully executing the procedure, no data is inserted into the Users table.
If I run the insert statement directly, it works.
Below is the full procedure code, before you ask me, the database is empty.
--USERS
CREATE PROCEDURE [dbo].[RegisterUser]
#NAME VARCHAR(255),
#PHONENUMBER VARCHAR(255),
#STATUS INT OUT,
#REMAININGDAYS INT OUT
AS
BEGIN
SET NOCOUNT ON;
UPDATE Users WITH (serializable)
SET Name = #NAME
WHERE PhoneNumber LIKE #PHONENUMBER
SET #REMAININGDAYS = 0
IF ##rowcount = 0
BEGIN
INSERT INTO Users (Name, PhoneNumber, RegisterDate)
VALUES (#NAME, #PHONENUMBER, GETDATE())
SET #STATUS = 0
SET #REMAININGDAYS = 40
END
ELSE
BEGIN
DECLARE #USERID BIGINT
DECLARE #EXP DATETIME
SELECT TOP 1
#USERID = USERID
FROM USERS
WHERE PhoneNumber LIKE #PHONENUMBER
SELECT TOP 1
#EXP = DATEADD(day, DAYS, REGISTERDATE)
FROM SUBSCRIPTIONS
WHERE USERID = #USERID
ORDER BY [REGISTERDATE]
IF #EXP IS NULL
BEGIN
SELECT TOP 1
#EXP = DATEADD(day, 40, REGISTERDATE)
FROM USERS
WHERE USERID = #USERID
IF GETDATE() < #EXP
BEGIN
SET #STATUS = 0
SET #REMAININGDAYS = DATEDIFF(day, GETDATE(), #EXP)
END
ELSE
BEGIN
SET #STATUS = -1
END
END
ELSE
BEGIN
IF GETDATE() < #EXP
SET #STATUS = 1
ELSE
SET #STATUS = -1
END
END
END
I call it passing all parameters.
Thank you!
Statements that make a simple assignment always set the ##ROWCOUNT value to 1. ##ROWCOUNT (Transact-SQL)
So
DECLARE #i int
SET #i = 0
PRINT ##ROWCOUNT
prints 1.
DECLARE #RC INT
UPDATE Users WITH (serializable) SET Name = #NAME
WHERE PhoneNumber LIKE #PHONENUMBER
SET #RC = ##ROWCOUNT
SET #REMAININGDAYS = 0
IF ##RC = 0
BEGIN
INSERT INTO Users <etc...>
Or move SET #REMAININGDAYS = 0 above the update statement so nothing between the update and the test of ##ROWCOUNT.

Stored Procedure Error - Transaction Count mismatch

I've been doing this stored procedure, however when I execute the stored procedure, I get an infinity execution. This cause a deadlock.
This is the error I got, can someone please help me on this?? Thanks.
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 2.
Code:
ALTER PROCEDURE [dbo].[spMaterialReceivingCreateItemRequirements]
#DomainSite nvarchar(8),
#ItemNo nvarchar(18),
#tReceiving_id integer,
#SampleRequired integer,
#UserName nvarchar(50)
AS
BEGIN
Declare #ErrorNo integer = '',
#New_JobNo integer,
#Status nvarchar(50) = 'InProcess',
#SPName nvarchar(max) = '',
#intSampleNo integer =1,
#ErrorMessage nvarchar(max) = ''
begin transaction t1
Begin try
BEGIN
--Generate 1 sample for item requirements
set #SampleRequired = 1
WHILE (#intSampleNo <= #SampleRequired)
BEGIN
insert into tItemRequirements
select
domainSite, #tReceiving_id, #ItemNo,
WorkCenter, tStationsType_id,
tSpecTestParameters_descriptions_id,
--row_number() OVER (ORDER BY ID) AS CurrentSet,
1 AS CurrentSet,
#intSampleNo, 1, 'InComplete', getdate(), #UserName
from
tspectestparameters
where
itemno = #ItemNo
set #intSampleNo = #intSampleNo +1
end
END
END TRY
Begin catch
SELECT
#ErrorNo = ERROR_NUMBER(),
#SPName = ERROR_PROCEDURE(),
#ErrorMessage = ERROR_MESSAGE();
rollback transaction t1
end catch
END
BEGIN TRANSACTION t1
BEGIN TRY
BEGIN
--Generate 1 sample for item requirements
SET #SampleRequired = 1
WHILE (#intSampleNo <= #SampleRequired)
BEGIN
INSERT INTO tItemRequirements
SELECT domainSite
, #tReceiving_id
, #ItemNo
, WorkCenter
, tStationsType_id
, tSpecTestParameters_descriptions_id
,
--row_number() OVER (ORDER BY ID) AS CurrentSet,
1 AS CurrentSet
, #intSampleNo
, 1
, 'InComplete'
, getdate()
, #UserName
FROM tspectestparameters
WHERE itemno = #ItemNo
SET #intSampleNo = #intSampleNo + 1
END
END
COMMIT
END TRY
BEGIN CATCH
SELECT #ErrorNo = ERROR_NUMBER()
, #SPName = ERROR_PROCEDURE()
, #ErrorMessage = ERROR_MESSAGE();
ROLLBACK TRANSACTION t1
END CATCH