Correct locking for stored procedure - sql-server

Apologies for the basic nature of this question by SQL, but it comes from a SQL noob.
I've created the following stored procedure after some online research. The aim of the the procedure is to maintain count (VisitCount), so the appropriate locking is necessary to maintain integrity. As far as I understand MERGE gives the correct level of lock for this scenario but I'd appreciate it if someone could advise whether this is correct or not.
Thanks.
ALTER PROCEDURE dbo.Popularity_Update
#TermID int
AS
SET NOCOUNT ON
DECLARE #Now date = SYSDATETIME()
BEGIN TRY
MERGE Popularity AS t
USING (SELECT #TermID AS TermID, #Now AS VisitDate) AS s ON t.TermID = s.TermID
AND t.VisitDate = s.VisitDate
WHEN MATCHED THEN
UPDATE
SET VisitCount += 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (TermID, VisitDate, VisitCount)
VALUES (s.TermID, s.VisitDate, 1);
END TRY
BEGIN CATCH
END CATCH

How about this....
ALTER PROCEDURE dbo.Popularity_Update
#TermID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Now date = SYSDATETIME()
BEGIN TRY
UPDATE Popularity
SET VisitCount = COALESCE(VisitCount, 0) + 1
WHERE TermID = #TermID
AND VisitDate = #Now
IF (##ROWCOUNT = 0)
BEGIN
INSERT INTO Popularity (TermID, VisitDate, VisitCount)
VALUES (#TermID, #Now, 1)
END
END TRY
BEGIN CATCH
END CATCH
END

Related

SQL Server - Changes not reflecting everytime until manually doing CHECKPOINT

I am having an issue in SQL Server procedure.
I have two new stored procedures, with the PROC_Main proc performing a bunch of inserts and updates before it calls the PROC_child to pull the updated records back out.
--Child PROC
CREATE PROCEDURE dbo.Proc_Child
#Id int
AS
BEGIN
SELECT * FROM dbo.Employee WHERE Id = #Id AND Status=1
END
--Parent Proc
CREATE PROCEDURE dbo.Proc_Main
#Id int ,#Status varchar(100),#Date datetime
AS
BEGIN
BEGIN TRY
BEGIN TRAN
IF NOT EXISTS (SELECT Id FROM dbo.Employee WHERE Id = #Id)
BEGIN
UPDATE dbo.Employee
SET Status = 3,
Date = getdate()
WHERE Status <> 3
AND Id = #Id
INSERT INTO dbo.Employee (ID,Status,Date)
VALUES (#ID,#Status,#Date)
END
COMMIT
--CHECKPOINT;
EXEC dbo.Proc_Child #Id = #Id
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN
DECLARE #Message VARCHAR(1000) = ERROR_MESSAGE()
DECLARE #Severity INT = ERROR_SEVERITY()
DECLARE #State INT = ERROR_STATE()
RAISERROR(#Message, #Severity, #State)
END CATCH
END
--Procedure call
EXEC Proc_Main #ID=1,#Status=1,#Date='2019-01-01'
I am facing the issue that Proc_Main is not returning the records from PROC_Child every time.
When I am manually doing checkpoint before Proc_Child is called then only it is returning records.
Nothing to do with checkpoint. Based on your code, if you call main proc with Status != 1, your child proc will not return it. Also, why are you doing update if you know that record does not exist? Finally, in the multi-threaded environment this may blow up, you need to lock the id when you checking for the existence.

How to get and use the value returned by a stored procedure to a INSERT INTO... SELECT... statement

I am just new in SQL language and still studying it. I'm having hard time looking for answer on how can I use the stored procedure and insert value into a table.
I have this stored procedure:
CREATE PROCEDURE TestID
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NewID VARCHAR(30),
#GenID INT,
#BrgyCode VARCHAR(5) = '23548'
SET #GenID = (SELECT TOP (1) NextID
FROM dbo.RandomIDs
WHERE IsUsed = 0
ORDER BY RowNumber)
SET #NewID = #BrgyCode + '-' + CAST(#GenID AS VARCHAR (30))
UPDATE dbo.RandomIDs
SET dbo.RandomIDs.IsUsed = 1
WHERE dbo.RandomIDs.NextID = #GenID
SELECT #NewID
END;
and what I'm trying to do is this:
INSERT INTO dbo.Residents([ResidentID], NewResidentID, [ResLogdate],
...
SELECT
[ResidentID],
EXEC TestID ,
[ResLogdate],
....
FROM
source.dbo.Resident;
There is a table dbo.RandomIDs containing random 6 digit non repeating numbers where I'm pulling out the value via the stored procedure and updating the IsUsed column of the table to 1. I'm transferring data from one database to another database and doing some processing on the data while transferring. Part of the processing is generating a new ID with the required format.
But I can't get it to work Sad I've been searching the net for hours now but I'm not getting the information that I need and that the reason for my writing. I hope someone could help me with this.
Thanks,
Darren
your question is little bit confusing, because you have not explained what you want to do. As i got your question, you want to fetch random id from randomids table and after performed some processing on nextid you want to insert it into resident table [newresidentid] and end of the procedure you fetch data from resident table. if i get anything wrong feel free to ask me.
your procedure solution is following.
CREATE PROCEDURE [TestId]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NEWID NVARCHAR(30)
DECLARE #GENID BIGINT
DECLARE #BRGYCODE VARCHAR(5) = '23548'
DECLARE #COUNT INTEGER
DECLARE #ERR NVARCHAR(20) = 'NO IDS IN RANDOM ID'
SET #COUNT = (SELECT COUNT(NEXTID) FROM RandomIds WHERE [IsUsed] = 0)
SET #GENID = (SELECT TOP(1) [NEXTID] FROM RandomIds WHERE [IsUsed] = 0 ORDER BY [ID] ASC)
--SELECT #GENID AS ID
IF #COUNT = 0
BEGIN
SELECT #ERR AS ERROR
END
ELSE
BEGIN
SET #NEWID = #BRGYCODE + '-' + CAST(#GENID AS varchar(30))
UPDATE RandomIds SET [IsUsed] = 1 WHERE [NextId] = #GENID
INSERT INTO Residents ([NewResidentId] , [ResLogDate] ) VALUES (#NEWID , GETDATE())
SELECT * FROM Residents
END
END
this procedure will fetch data from your randomids table and perform some processing on nextid than after it directs insert it into resident table and if you want to insert some data through user you can use parameter after declaring procedure name
E.G
CREATE PROCEDURE [TESTID]
#PARAM1 DATATYPE,
#PARAM2 DATATYPE
AS
BEGIN
END
I'm not convinced that your requirement is a good one but here is a way to do it.
Bear in mind that concurrent sessions will not be able to read your update until it is committed so you have to kind of "lock" the update so you will get a block until you're going to commit or rollback. This is rubbish for concurrency, but that's a side effect of this requirement.
declare #cap table ( capturedValue int);
declare #GENID int;
update top (1) RandomIds set IsUsed=1
output inserted.NextID into #cap
where IsUsed=0;
set #GENID =(select max( capturedValue) from #cap )
A better way would be to use an IDENTITY or SEQUENCE to solve your problem. This would leave gaps but help concurrency.

Get Key values of the Row whose values are not Updated during Multiple row update

This is a Continuation of my previous question
sql update for dynamic row number
This time I am having an updated requirement.
I am having 2 tables
CraftTypes & EmployeeCraftTypes.
I need to update multiple rows in the CraftType Table and
I was able to update it as per the answer provided by TheGameiswar
Now there is a modification in the requirement.
In the table CraftTypes, there is a foreign key reference for the column CraftTypeKey with the table EmployeeCraftsTypes.
If there exist an entry for CraftTypeKey in the EmployeeCrafttypes table, then the row should not be updated.
Also the CraftTypeKey's whose row values are not updated must be obtained for returning the FK_restriction status of the rows.
This is the sql query I am using.
CREATE TYPE [DBO].[DEPARTMENTTABLETYPE] AS TABLE
( DepartmentTypeKey SMALLINT, DepartmentTypeName VARCHAR(50),DepartmentTypeCode VARCHAR(10) , DepartmentTypeDescription VARCHAR(128) )
ALTER PROCEDURE [dbo].[usp_UpdateDepartmentType]
#DEPARTMENTDETAILS [DBO].[DEPARTMENTTABLETYPE] READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #rowcount1 INT
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE D1
SET
D1.[DepartmentTypeName]=D2.DepartmentTypeName
,D1.[DepartmentTypeCode]=D2.DepartmentTypeCode
,D1.[DepartmentTypeDescription]=D2.DepartmentTypeDescription
FROM
[dbo].[DepartmentTypes] D1
INNER JOIN
#DEPARTMENTDETAILS D2
ON
D1.DepartmentTypeKey=D2.DepartmentTypeKey
WHERE
D2.[DepartmentTypeKey] not in (select 1 from [dbo].[EmployeeDepartment] where [DepartmentTypeKey]=D2.DepartmentTypeKey)
SET #ROWCOUNT1=##ROWCOUNT
COMMIT
END TRY
BEGIN CATCH
SET #ROWCOUNT1=0
ROLLBACK TRAN
END CATCH
IF #rowcount1 =0
SELECT -174;
ELSE
SELECT 100;
END
END
Please Help
And Thanks in Advance
Ok
I think I figured out a way for it this time. I am not sure this is the right way, but its enough for me to meet the requirements.
I selected the distinct rows with FK reference from EmployeeCraftsTypes table as a second select query.
Now I can get the Row values which are not getting updated due to FK constraint.
This is the sql query I have used
ALTER PROCEDURE [dbo].[usp_UpdateCraftType]
#CRAFTDETAILS [DBO].[CRAFTTABLETYPE] READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #STATUSKEY TINYINT = (SELECT DBO.GETSTATUSKEY('ACTIVE'))
DECLARE #ROWCOUNT1 INT
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE C1
SET
[C1].[CraftTypeName]=C2.CRAFTTYPENAME
,[C1].[CRAFTTYPEDESCRIPTION]=C2.CRAFTTYPEDESCRIPTION
,[C1].[StatusKey]=C2.[StatusKey]
FROM
[dbo].[CRAFTTYPES] C1
INNER JOIN
#CRAFTDETAILS C2
ON
C1.CRAFTTYPEKEY=C2.CRAFTTYPEKEY
WHERE
C2.[CRAFTTYPEKEY] NOT IN (SELECT EC.[CRAFTTYPEKEY] from [dbo].[EmployeeCrafts] EC where EC.[CRAFTTYPEKEY]=C2.[CRAFTTYPEKEY])
SET #ROWCOUNT1=##ROWCOUNT
COMMIT
END TRY
BEGIN CATCH
SET #ROWCOUNT1=0
ROLLBACK TRAN
END CATCH
--SET #ROWCOUNT1 = ##ROWCOUNT
IF #ROWCOUNT1 =0
SELECT -172;
ELSE
BEGIN
SELECT 100;
SELECT DISTINCT EC.[CRAFTTYPEKEY],'Value Already Assigned' as Reason
FROM [DBO].[EmployeeCrafts] EC
JOIN #CRAFTDETAILS C3
on C3.[CRAFTTYPEKEY]=EC.[CRAFTTYPEKEY]
WHERE EC.[CRAFTTYPEKEY]=C3.[CRAFTTYPEKEY]
END
END
END
Now in the Web API side I can check if there is any update failure by checking the rowcount for the second table.
If the row count is more than 0, then update error message can be generated
Hope it will be helpful to someone ....

Transaction is rolled back after commit?

I'm experiencing some problems that look a LOT like a transaction in a stored procedure has been rolled back, even though I'm fairly certain that it was committed, since the output variable isn't set until after the commit, and the user gets the value of the output variable (I know, because they print it out and I also set up a log table where i input the value of the output variable).
In theory someone COULD manually delete and update the data such that it would look like a rollback, but it is extremely unlikely.
So, I'm hoping someone can spot some kind of structural mistake in my stored procedure. Meet BOB:
CREATE procedure [dbo].[BOB] (#output_id int OUTPUT, #output_msg varchar(255) OUTPUT)
as
BEGIN
SET NOCOUNT ON
DECLARE #id int
DECLARE #record_id int
SET #output_id = 1
-- some preliminary if-statements that doesn't alter any data, but might do a RETURN
SET XACT_ABORT ON
BEGIN TRANSACTION
BEGIN TRY
--insert into table A
SET #id = SCOPE_IDENTITY()
--update table B
DECLARE csr cursor local FOR
SELECT [some stuff] and record_id
FROM temp_table_that_is_not_actually_a_temporary_table
open csr
fetch next from csr into [some variables], #record_id
while ##fetch_status=0
begin
--check type of item + if valid
IF (something)
BEGIN
SET SOME VARIABLE
END
ELSE
BEGIN
ROLLBACK TRANSACTION
SET #output_msg = 'item does not exist'
SET #output_id = 0
RETURN
END
--update table C
--update table D
--insert into table E
--execute some other stored procedure (without transactions)
if (something)
begin
--insert into table F
--update table C again
end
DELETE FROM temp_table_that_is_not_actually_a_temporary_table WHERE record_id=#record_id
fetch next from csr into [some variables], #record_id
end
close csr
deallocate csr
COMMIT TRANSACTION
SET #output_msg = 'ok'
SET #output_id = #id
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SET #output_msg = 'transaction failed !'
SET #output_id = 0
INSERT INTO errors (record_time, sp_name, sp_msg, error_msg)
VALUES (getdate(), 'BOB', #output_msg, error_message())
END CATCH
RETURN
END
I know, my user gets an #output_id that is the SCOPE_IDENTITY() and he also gets an #output_msg that says 'ok'. Is there ANY way he can get those outputs without the transaction getting committed?
Thank you.
You know the problem is that transaction dose NOT support rollback on variables because there is no data change inside database. Either commit or rollback of the transactions ONLY make difference on those database objects (tables, temp table, etc.), NOT THE VARIABLES (including table variables).
--EDIT
declare #v1 int = 0, #v2 int = 0, #v3 int = 0
set #v2 = 1
begin tran
set #v1 = 1
commit tran
begin tran
set #v3 = 1
rollback tran
select #v1 as v1, #v2 as v2, #v3 as v3
RESULT is as follows
Personally I never used transactions in stored procedures, especially when they are used simultaniously by many people. I seriously avoid cursors as well.
I think I would go with passing the involved rows of temp_table_that_is_not_actually_a_temporary_table into a real temp table and then go with an if statement for all rows together. That's so simple in tsql:
select (data) into #temp from (normal_table) where (conditions).
What's the point of checking each row, doing the job and then rollback the whole thing if say the last row doesn't meet the condition? Do the check for all of them at once, do the job for all of them at once. That's what sql is all about.

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

Resources