Prevent concurrent access to stored procedure in sql server - sql-server

i have sequences table that consists of three columns:
Number,Year,Type
and for each new year a three new records gets created and updated all along this year.
my stored procedure for generating the sequence is used inside other stored procedures, and my issue is that i want to block concurrent access to this stored procedure and make the access as queue so if concurrent access occur one has to wait for another to finish so that two users don't get same sequence number, the code is as follows:
ALTER PROCEDURE [dbo].[GETSEQUENECENO]
#p_hijricYear INT ,
#p_typeId INT ,
#return_val INT OUTPUT
AS
BEGIN
DECLARE #newSequence INT
BEGIN TRY
SELECT #return_val = 0
SELECT #newSequence = ISNULL( max(correspondencenumber) ,0 )
FROM io_sequencenumbers with (XLOCK)
WHERE
hijricyear = #p_hijricyear
AND
typeid = #p_typeid
END TRY
BEGIN CATCH
SELECT #newSequence = -1
END CATCH
IF #newSequence != -1
BEGIN
IF #newSequence = 0
BEGIN
SELECT #newSequence = 1
INSERT INTO io_sequencenumbers
VALUES
( #newSequence ,
#p_hijricYear ,
#p_typeId )
END
ELSE
BEGIN
SELECT #newSequence = #newSequence + 1
UPDATE io_sequencenumbers
SET
correspondencenumber = #newSequence
WHERE hijricyear = #p_hijricyear
AND
typeid = #p_typeid
END
END -- end of #newSequence!= -1 --
SELECT #return_val = #newSequence
END
i read that setting isolation level to serializable may solve it, is that enough or i have to use also begin and end transaction in stored procedure and manually handling rollback and commit ?

One approach could be the use of SQL Server application locks, see sp_getapplock and sp_releaseapplock. This will let you serialise your sequence generation through the SP without the need for serialisable transactions but won't prevent access to the io_sequecenumbers table by other code so you'll need to be sure that this SP is the only place that updates this table.

I was able to optimize the sequence generation this way:
ALTER PROCEDURE [dbo].[GETSEQUENECENO]
#p_hijricYear INT ,
#p_typeId INT ,
#return_val INT OUTPUT
AS
BEGIN
DECLARE #newSequence numeric(18,0)
BEGIN TRY
UPDATE IO_SEQUENCENUMBERS WITH (READCOMMITTEDLOCK)
SET #newSequence = correspondencenumber = correspondencenumber + 1
WHERE
hijricyear = #p_hijricyear
AND
typeid = #p_typeid
END TRY
BEGIN CATCH
SELECT #newSequence = -1
END CATCH
SELECT #return_val = #newSequence
END

Related

How to force a deadlock to test a stored procedure's try/catch?

My procedure spUmowyXMLintoLOG looks like :
CREATE PROCEDURE dbo.spUmowyXMLintoLOG
(
#inXML XML,
#PROCID INT,
#idRekorduZrodlowego INT,
#idTabeliZrodlowej INT,
#TrybWywolania INT, /* 0 pierwszy wpis z inXML,
1 drugi wpis z outXML,
-1 czytanie z logu po nazwie obiektu idRekorduZrodloweho i idTabeliZrodlowej */
#outIdWpisuDoLogu INT OUTPUT,
#dataOd DATETIME,
#dataDo DATETIME,
#CzyBlad BIT,
#ErrorMessage VARCHAR(4000),
#uidOperacji VARCHAR(255) = NULL /* UM2-4818 */
)
AS
BEGIN
DECLARE #komunikatPrint VARCHAR(1000)
DECLARE #NazwaObiektu VARCHAR(255)
SELECT #NazwaObiektu = name
FROM dbo.sysobjects WITH(NOLOCK)
WHERE id = #PROCID
BEGIN TRY
IF #TrybWywolania = 0
BEGIN
INSERT INTO dbo.UmowyXMLzInterfejsow_log2(ProcID,NazwaObiektu,inXml,CzyBlad,UIDOperacji)
SELECT #PROCID,#NazwaObiektu,#inXML,#CzyBlad,#uidOperacji
SELECT #outIdWpisuDoLogu = SCOPE_IDENTITY()
/* zamapowanie szczegółów */
INSERT INTO UmowyXMLzInterfejsow_log2_szczegoly WITH(XLOCK,ROWLOCK)
SELECT #outIdWpisuDoLogu,idRekorduZrodlowego,idTabeliZrodlowej
FROM #UmowyXMLzInterfejsow_log_szczegoly WITH(NOLOCK)
/* UM2-4842 BEGIN zapis uida biznesowego */
IF OBJECT_ID('tempdb..#umowyXMLzInterfejsow_log_UIDbiznesowy') IS NOT NULL
BEGIN
INSERT INTO dbo.UmowyXMLzInterfejsow_log2_UIDbiznesowy (idWpisuDoLogu,UIDBiznesowy)
SELECT #outIdWpisuDoLogu,UIDBiznesowy
FROM #umowyXMLzInterfejsow_log_UIDbiznesowy
END
/* UM2-4842 END zapis uida biznesowego */
END
ELSE
IF #TrybWywolania = 1
BEGIN
UPDATE dbo.UmowyXMLzInterfejsow_log2 WITH(XLOCK,ROWLOCK)
SET outXml = #inXML,
DataAktualizacji = GETDATE(),
CzyBlad = #CzyBlad,
ErrorMessage = #ErrorMessage
WHERE idWpisuDoLogu = #outIdWpisuDoLogu
END
ELSE
IF #TrybWywolania = -1
BEGIN
IF ISNULL(#PROCID,0) <> 0
AND ISNULL(#idRekorduZrodlowego,0) <> 0
AND ISNULL(#idTabeliZrodlowej,0) <> 0
BEGIN
SELECT 'Wyszukianie po idRekorduZrodlowego,idTabeliZrodlowej -> dbo.UmowyXMLzInterfejsow_log',l.*,'_szczegoly',ls.*,'SWUIM_SystemyTabeleZrodlowe',stz.*
FROM dbo.UmowyXMLzInterfejsow_log2 l WITH(NOLOCK)
JOIN UmowyXMLzInterfejsow_log2_szczegoly ls WITH(NOLOCK)
ON l.idWpisuDoLogu = ls.idWpisuDoLogu
LEFT JOIN SWUIM_SystemyTabeleZrodlowe stz WITH(NOLOCK)
ON ls.idTabeliZrodlowej = stz.id
WHERE ls.idRekorduZrodlowego = #idRekorduZrodlowego
AND ls.idTabeliZrodlowej = #idTabeliZrodlowej
END
END
IF #TrybWywolania = -2
BEGIN
IF ISNULL(#PROCID,0) <> 0
AND ISNULL(#dataOd,'9999-12-31') <= CONVERT(VARCHAR(10),GETDATE(),120)
AND ISNULL(#dataDo,'9999-12-31') >= CONVERT(VARCHAR(10),GETDATE(),120)
BEGIN
SELECT 'Wyszukianie po dacieWpisu -> dbo.UmowyXMLzInterfejsow_log',l.*,'_szczegoly',ls.*,'SWUIM_SystemyTabeleZrodlowe',stz.*
FROM dbo.UmowyXMLzInterfejsow_log2 l WITH(NOLOCK)
JOIN UmowyXMLzInterfejsow_log2_szczegoly ls WITH(NOLOCK)
ON l.idWpisuDoLogu = ls.idWpisuDoLogu
LEFT JOIN SWUIM_SystemyTabeleZrodlowe stz WITH(NOLOCK)
ON ls.idTabeliZrodlowej = stz.id
WHERE l.NazwaObiektu = #NazwaObiektu
AND l.DataWpisu >= #dataOd
AND l.DataWpisu <= #dataDo
END
END
END TRY
BEGIN CATCH
SELECT #komunikatPrint = 'Wystapił problem z zapisem do XML do logu: ' + ERROR_MESSAGE()
END CATCH
END
As you can see this procedure get insert into :
INSERT INTO dbo.UmowyXMLzInterfejsow_log2(ProcID,NazwaObiektu,inXml,CzyBlad,UIDOperacji)
SELECT #PROCID,#NazwaObiektu,#inXML,#CzyBlad,#uidOperacji
Now i try to get deadlock while execute this procedure to test .
I open 2 query windows and in first one i try to lock table like this :
BEGIN TRY
begin tran az
select top 10 * from UmowyXMLzInterfejsow_log2 with(tablockx)
WAITFOR DELAY '00:0:30'
commit tran az
END TRY
BEGIN CATCH
rollback tran az
END CATCH
The second window is just execute this procedure - to get deadlock while inserting data
begin tran az
DECLARE #outIdWpisuDoLogu INT
IF OBJECT_ID('tempdb..#umowyXMLzInterfejsow_log_szczegoly') IS NOT NULL
DROP TABLE #umowyXMLzInterfejsow_log_szczegoly
CREATE TABLE #umowyXMLzInterfejsow_log_szczegoly
(
idWpisuDoLogu [int] NULL,
idRekorduZrodlowego [int] NOT NULL,
idTabeliZrodlowej [int] NOT NULL
)
EXEC dbo.spUmowyXMLintoLOG 'xx',##procid,null,null,0,#outIdWpisuDoLogu OUTPUT,NULL,NULL,0,NULL
select #outIdWpisuDoLogu
rollback tran az
After when i lock table - the procedure just executing 30 sec then output comes out without problem.
When i change delay time to 10 min it just execute 10 min...
How can i get deadlock here? - it should appears because table is locked by another transaction.
Deadlocks aren't locks. They are conflicting locks. For example:
sp1 locks table a, then table b.
sp2 locks table b, then table a.
If you run the two sps concurrently, sp1 locks table a and then tries to lock table b. But sp2 has already locked b so sp1 waits for it to be unlocked. At the same time sp2 has locked table b so sp1 waits. They're both waiting, until SQL Server detects the situation and terminates one of the operations to break the deadlock.
Your database is functioning as designed, by delaying execution of one sp until another sp releases the locks it has.
To force a deadlock, you'll need two resources (tables) to lock, and two SQL sequences from separate database connections to lock them in the opposite order. You could do this with one stored procedure and a SSMS session. Start the SSMS session with SET DEADLOCK_PRIORITY HIGH; so SQL Server kills your SP rather than randomly picking your SSMS session to kill.
Consider Monty Python's Australian Philosophers sitting around a table with a big plate of spaghetti, and just one fork and one spoon they must share. To eat, each philosopher needs to use both the spoon and the fork and then put them down. If one grabs the fork first and another grabs the spoon first, they go hungry. But if all philosophers grab the fork first, then the spoon, they can eat one at a time.

Stored procedure inserting the same record repeatedly instead of looping through list from SELECT

I am fairly new at writing procedures (beyond the basics)
I am trying to write a stored procedure that inserts into a table (dbo.billing_batch) based on a select statement that loops through the list of results (#DealerID FROM dbo.vehicle_info).
The SELECT DISTINCT... statement on its own works perfectly and returns a list of 54 records.
The result of the SELECT statement is dynamic and will change from week to week, so I cannot count on 54 records each time.
I am trying to use WHILE #DealerID IS NOT NULL to loop through the INSERT routine.
The loop is supposed to update dbo.billing_batch, however it is inserting the same 1st record (BillingBatchRosterID, DealerID) over and over and over to infinity.
I know I must be doing something wrong (I have never written a stored procedure that loops).
Any help would be greatly appreciated!
Here is the stored procedure code:
ALTER PROCEDURE [dbo].[sp_billing_batch_set]
#varBillingBatchRosterID int
AS
SET NOCOUNT ON;
BEGIN
DECLARE #DealerID int
SELECT DISTINCT #DealerID = vi.DealerID
FROM dbo.vehicle_info vi
LEFT JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4 OR vi.ItemStatusID = 5 OR vi.ItemStatusID = 8)
END
WHILE #DealerID IS NOT NULL
BEGIN TRY
INSERT INTO dbo.billing_batch (BillingBatchRosterID, DealerID)
VALUES(#varBillingBatchRosterID, -- BillingBatchRosterID - int
#DealerID) -- DealerID - int
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription
END CATCH
You have the same problems as another recent post here: Iterate over a table with a non-int id value
Why do a loop? Just do it as a single SQL statement
If you must use a loop, you will need to update your #Dealer value at each run (e.g., to the next DealerId) otherwise it will just infinitely loop with the same DealerID value
Don't do a loop.
Here's an example not needing a loop.
ALTER PROCEDURE [dbo].[P_billing_batch_set]
#varBillingBatchRosterID int
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
INSERT INTO dbo.billing_batch (DealerID, BillingBatchRosterID)
SELECT DISTINCT vi.DealerID, #varBillingBatchRosterID
FROM dbo.vehicle_info vi
INNER JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4
OR vi.ItemStatusID = 5
OR vi.ItemStatusID = 8
);
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription;
END CATCH;
END;
Note I
Changed the LEFT JOIN to an INNER JOIN as your WHERE clause needs the record to exist in the dealer_info table
Moved the SET NOCOUNT ON; to be within the BEGIN-END section
Moved the END to the end
Renamed your stored procedure as per the excellent comment from #marc_s (on the question itself)

Select a column from a row and update the column to another value

I am trying to write a stored procedure that reads a column in a particular row of a table, then updates that column with a new value. The orig. is returned.
I want it to lock the row from others till I am done. What is the process?
I have something like
CREATE PROCEDURE [dbo].[aptc_Prt_NextDocumentNumberGet]
(#_iFormatConfigID INT, #_oNextDocumentNumber FLOAT OUTPUT)
AS
BEGIN
DECLARE #FrameworkConfig XML
SET #_oNextDocumentNumber = - 1
DECLARE #NewNextDocumentID FLOAT
SELECT
#_oNextDocumentNumber = FrameworkConfig.value('(/Parameters/Parameter[#Name="NextDocNo.NextDocumentNumber"])[1]', 'float')
FROM
[ttcPrtFormatConfig] WITH (ROWLOCK)
WHERE
FormatConfigID = #_iFormatConfigID
-- Select the Next Doc num out of the xml field
-- increment appropriate control and set output
IF #_iFormatConfigID IS NOT NULL
BEGIN
-- set what will be the "next" doc number after we add this current txn
IF (ABS(#_oNextDocumentNumber - 99999999999999999) < 0.0001)
BEGIN
SELECT #NewNextDocumentID = 1
END
ELSE
BEGIN
SELECT #NewNextDocumentID = #_oNextDocumentNumber + 1
END
UPDATE [ttcPrtFormatConfig]
WITH (ROWLOCK)
SET FrameworkConfig.modify('
replace value of
(/Parameters/Parameter[#Name="NextDocNo.NextDocumentNumber"]/text())[1]
with sql:variable("#NewNextDocumentID")')
WHERE FormatConfigID = #_iFormatConfigID
END
END
This should get you close to what you want.
DECLARE #MyValue INT
--You need a transaction so that the scope of your lock is well defined
BEGIN TRANSACTION
BEGIN TRY
--Get the value you are interested in, This select will lock the row so other people will not even be able to read it until you are finished!!!!!
SELECT #MyValue = MyValue
FROM MyTable WITH (UPDLOCK HOLDLOCK)
WHERE MyValue = SomeValue
--Do your checks and updates. You can take as long as you like as you are the only person who can do a read or update of this data.
IF
BEGIN
UPDATE MyTable
END
--Make sure you commit or rollback! this will release the lock
END TRY
BEGIN CATCH
--Oh no bad stuff! give up and put it back to how it was
PRINT ERROR_MESSAGE() + N' Your message here'
--Check there is a transaction that we can rollback
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK;
END
--You may want to return some error state and not throw!
THROW;
--RETURN -1 --(for example)
END CATCH;
--yay it all worked and your lock will be released
COMMIT
--Do what you like with the old value
RETURN #MyValue

SQL Server : Query not running as needed

I am working with Sage Evolution and do a lot of the back end stuff to customize it for our company.
I need to write a query where, when a user enters a negative quantity the system must not allow the transaction, however when the user enters a negative quantity and the product belongs to the "chemicals" group it needs to process the transaction.
Here is my code I have written so far.
DECLARE
#iAfterfQuantity Int;
#iAfteriStockCodeID Int;
#iAfterStockItemGroup VarChar
SELECT
#iAfterfQuantity = fQuantity,
#iAfteriStockCodeID = iStockCodeID
FROM
INSERTED
SELECT
#iAfterStockItemGroup = ItemGroup
FROM
dbo.stkItem
WHERE
StockLink = #iAfteriStockCodeID
BEGIN
IF #iAfterfQuantity < 0 AND #iAfterStockItemGroup <> 'chemicals'
BEGIN
RAISERROR ('',16,1)
ROLLBACK TRANSACTION
END
END
This is a task better suited for a check constraint then for a trigger, especially considering the fact that you are raising an error.
First, create the check function:
CREATE FUNCTION fn_FunctionName
(
#iAfterfQuantity Int,
#iAfteriStockCodeID Int
)
RETURNS bit
AS
BEGIN
DECLARE #iAfterStockItemGroup VarChar(150) -- Must specify length!
SELECT #iAfterStockItemGroup = ItemGroup FROM dbo.stkItem WHERE StockLink=#iAfteriStockCodeID
IF #iAfterfQuantity < 0 AND #iAfterStockItemGroup <> 'chemicals'
RETURN 0
RETURN 1 -- will be executed only if the condition is false...
END
Then, alter your table to add the check constraint:
ALTER TABLE YourTableName
ADD CONSTRAINT ck_ConstraintName
CHECK (dbo.fn_FunctionName(fQuantity, iStockCodeID) = 1)
GO

Basic LINQ to SQL Question: How to call stored procedure and retrieve single return value

I'm new to LINQ and am having a problem getting proper results from a simple (working) stored procedure that takes no parameters and returns a single Integer. When calling this sproc with LINQ its returnvalue is always 0. When run using tableadapter or directly on the sql server it works, returning 6 digit values like 120123.
Here is my LINQ code:
Dim MeetingManager As New MeetingManagerDataContext
Dim MeetingID As Integer = MeetingManager.NewMeetingID().ReturnValue
Here is the NewMeetingID procedure:
ALTER PROCEDURE [dbo].[NewMeetingID]
AS
SET NOCOUNT ON
BEGIN
BEGIN TRANSACTION
SELECT UniqueNumber
FROM tblTakeANumber
WHERE NumberID = 1
BEGIN
UPDATE tblTakeANumber SET UniqueNumber = UniqueNumber + 1
WHERE (NumberID = 1)
END
COMMIT TRANSACTION
RETURN
END
Am I missing someting?
Return value is the value that a stored procedure returns, like:
create procedure TestProc()
as
return 1
But your stored procedure is probably selecting a result, like:
create procedure TestProc()
as
select 1 as Col1
To retrieve the first column, specify it by name:
Dim Col1 As Integer = TestDataCintext.TestProc().Single().Col1
EDIT: Per your comment, here's how you could modify the stored procedure to return the integer. Note the (updlock) and isolation level:
ALTER PROCEDURE [dbo].[NewMeetingID]
AS
SET NOCOUNT ON
BEGIN
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
declare #UniqueNumber int
SELECT #UniqueNumber = UniqueNumber
FROM tblTakeANumber (updlock)
WHERE NumberID = 1
UPDATE tblTakeANumber
SET UniqueNumber = #UniqueNumber + 1
WHERE NumberID = 1
COMMIT TRANSACTION
RETURN #UniqueNumber
END

Resources