Do stored procedures lock tables/rows? - sql-server

Quite a simple question. In SQL 2008 if I have a stored procedure (see below) do I run the risk of a race condition between the first two statements or does the stored procedure put a lock on the things it touches like transactions do?
ALTER PROCEDURE [dbo].[usp_SetAssignedTo]
-- Add the parameters for the stored procedure here
#Server varchar(50),
#User varchar(50),
#UserPool varchar(50)
AS
BEGIN
SET NOCOUNT ON;
Declare #ServerUser varchar(50)
-- Find a Free record
SELECT top 1 #ServerUser = UserName
from ServerLoginUsers
where AssignedTo is null and [TsServer] = #Server
--Set the free record to the user
Update ServerLoginUsers
set AssignedTo = #User, AssignedToDate = getdate(), SourcePool = #UserPool
where [TsServer] = #Server and UserName = #ServerUser
--report record back if it was updated. Null if it was not available.
select *
from ServerLoginUsers
where [TsServer] = #Server
and UserName = #ServerUser
and AssignedTo = #User
END

You could get a race condition.
It can be done in one statement:
You can assign in an UPDATE
The lock hints allow another process to skip this row
The OUTPUT clause returns data to the caller
Try this... (edit: holdlock removed)
Update TOP (1) ServerLoginUsers WITH (ROWLOCK, READPAST)
OUTPUT INSERTED.*
SET
AssignedTo = #User, AssignedToDate = getdate(), SourcePool = #UserPool
WHERE
AssignedTo is null and [TsServer] = #Server -- not needed -> and UserName = #ServerUser
If not, you may need a separate select
Update TOP (1) ServerLoginUsers WITH (ROWLOCK, READPAST)
SET
-- yes, assign in an update
#ServerUser = UserName,
-- write
AssignedTo = #User, AssignedToDate = getdate(), SourcePool = #UserPool
OUTPUT INSERTED.*
WHERE
AssignedTo is null and [TsServer] = #Server -- not needed -> and UserName = #ServerUser
SELECT ...
See this please for more: SQL Server Process Queue Race Condition

Related

How to clean up all entries in a cdc table in MS SQL?

Reading Microsoft Docs this is the relevant system procedure:
sys.sp_cdc_cleanup_change_table
I tried using it like this:
DECLARE #max_lsn binary(10);
SET #max_lsn = sys.fn_cdc_get_max_lsn();
Exec sys.sp_cdc_cleanup_change_table
#capture_instance = N'dbo_mytable',
#low_water_mark = #max_lsn;
The query is executed successfully but checking the table again with the query:
DECLARE #from_lsn binary(10), #to_lsn binary(10);
SET #from_lsn = sys.fn_cdc_get_min_lsn('dbo_mytable');
SET #to_lsn = sys.fn_cdc_get_max_lsn();
SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_mytable
(#from_lsn, #to_lsn, N'all');
Still returns a non empty table. I'm not familiar with SQL much. What am I missing?
I built a little test for this, and yes, I saw the same thing. It took me a couple of minutes to figure out what was going on.
The "gotcha" is this little entry in the docs:
If other entries in cdc.lsn_time_mapping share the same commit time as the entry identified by the new low watermark, the smallest LSN associated with that group of entries is chosen as the low watermark.
In other words, if the result of sys.fn_cdc_get_max_lsn() maps to a cdc.lsn_time_mapping.tran_begin_time that also has other start_lsn values associated with it, then the cleanup proc won't actually use the value of sys.fn_cdc_get_max_lsn() as the new low water mark.
In other other words, if the max lsn currently in the change table you want to clean up has the same tran_begin_time as other LSN's, and it is not the lowest of those LSNs, you cannot get a "complete" cleanup of the change table.
The easiest way to get a complete cleanup in those cases is probably to make a minor change to the target table to advance the max lsn and force a new entry, and "hope" that the new entry isn't also associated with any other LSNs with the same tran begin time.
To make that more explicit, here's my little test. Running it over and over has a result that in some cases cleanup is predicted to fail (and fails) and in other cases it is predicted to succeed (and succeeds).
/*
one time setup:
create table t(i int primary key, c char);
create table u(i int primary key, d char);
go
exec sp_cdc_enable_db;
go
exec sys.sp_cdc_enable_table #source_schema = 'dbo',
#source_name = 't',
#supports_net_changes = 1,
#role_name = null;
exec sys.sp_cdc_enable_table #source_schema = 'dbo',
#source_name = 'u',
#supports_net_changes = 1,
#role_name = null;
*/
set nocount on;
delete from t;
delete from u;
go
insert t select 1, 'a';
insert u select 1, 'b';
waitfor delay '00:00:01';
go
declare #fail int;
select #fail = count(*)
from cdc.lsn_time_mapping
where tran_begin_time = (
select tran_begin_time
from cdc.lsn_time_mapping
where start_lsn = sys.fn_cdc_get_max_lsn()
);
print iif(#fail > 1, 'this wont do the cleanup you expect', 'this will do the cleanup');
DECLARE #max_lsn binary(10) = sys.fn_cdc_get_max_lsn();
Exec sys.sp_cdc_cleanup_change_table
#capture_instance = N'dbo_t',
#low_water_mark = #max_lsn;
go
if exists (select * from cdc.dbo_t_ct) print 'did not cleanup';
else print 'did the cleanup';

MSSQL2016 UPDATE trigger to INSERT record in other table based on SUM of bit column

I'm attempting to automate our order status update system. Here is the flow we have in place:
orders come in to our system as one order (via FTP file), and get split into 2 or more orders (inserted into SplitOrdersHeader_tb)
resulting multiple orders are sent from the SplitOrders tables (header and detail) to ERP system
ERP system produces order confirmations for each order (inserted into OrderConfirmationHeader_tb, and detail table)
need to update original single order status after all split order confirmations have been received
Here are the tables involved:
CREATE TABLE SplitOrdersHeader_tb(
OriginalCustomerPONumber varchar(20),
NewCustomerPONumber varchar(20),
Company varchar(2),
CustomerNumber varchar(10),
OrderProcessed bit DEFAULT 0,
OrderMoved bit DEFAULT 0,
OrderConfirmationReceived bit DEFAULT 0
)
CREATE TABLE OrderConfirmationHeader_tb(
MasterOrderNumber varchar(20),
CustomerPONumber varchar(20),
Company varchar(2),
CustomerNumber varchar(10)
)
CREATE TABLE UpdateOtherSystem_tb(
OriginalCustomerPONumber varchar(20)
)
I have a trigger on the OrderConfirmationHeader_tb that updates the status of each split order, once the order confirmations have been loaded:
CREATE TRIGGER dbo.INSERT_Update_SplitOrderConfirmations_tg
ON dbo.OrderConfirmationHeader_tb
AFTER INSERT
AS
SET NOCOUNT ON
BEGIN
UPDATE SOH
SET SOH.OrderConfirmationReceived = 1,
SOH.MasterOrderNumber = LTRIM(RTRIM(I.MasterOrderNumber))
FROM OrderSplitting.SplitOrdersHeader_tb SOH
INNER JOIN inserted I ON SOH.CustomerNumber = I.Customer
AND SOH.NewCustomerPONumber = I.Reference
AND SOH.Company = I.Company
AND SOH.OrderProcessed = 1
AND SOH.OrderMoved = 1
END
What I'm wanting to do is create an UPDATE trigger on the SplitOrdersHeader_tb that will:
- count the number of split orders from the original CustomerPONumber
- sum the number of OrderConfirmationReceived values
- if COUNT = SUM then insert a new record into UpdateOtherSystem_tb, provided the MasterOrderNumber does not already exist in the UpdateOtherSystem_tb
I have this, but it feels way too clunky:
CREATE TRIGGER OrderSplitting.UPDATE_Update_WCO_Status_tg
ON OrderSplitting.SplitOrdersHeader_tb
AFTER UPDATE AS
SET NOCOUNT ON
BEGIN
DECLARE #NEW_CUSTOMER_PO_NUMBER varchar(255),
#ORIGINAL_CUSTOMER_PO_NUMBER varchar(255),
#COUNT_OF_ORDER_HEADERS int,
#TOTAL_CONFIRMED_ORDERS int
SELECT #NEW_CUSTOMER_PO_NUMBER = NewCustomerPONumber
FROM inserted
SELECT #ORIGINAL_CUSTOMER_PO_NUMBER = OriginalCustomerPONumber,
#COUNT_OF_ORDER_HEADERS = COUNT(*),
#TOTAL_CONFIRMED_ORDERS = SUM(CAST(OrderConfirmationReceived as int))
FROM OrderSplitting.SplitOrdersHeader_tb
WHERE OriginalCustomerPONumber IN (SELECT OriginalCustomerPONumber
FROM OrderSplitting.SplitOrdersHeader_tb
WHERE NewCustomerPONumber = #NEW_CUSTOMER_PO_NUMBER)
GROUP BY OriginalCustomerPONumber
IF #COUNT_OF_ORDER_HEADERS = #TOTAL_CONFIRMED_ORDERS
BEGIN
BEGIN TRY
INSERT INTO OrderSplitting.UpdateOtherSystem_tb(OriginalCustomerPONumber)
VALUES(#ORIGINAL_CUSTOMER_PO_NUMBER)
END TRY
BEGIN CATCH
DECLARE #ERROR_MESSAGE varchar(MAX)
SET #ERROR_MESSAGE = ERROR_MESSAGE()
EXEC msdb..sp_send_dbmail
#recipients = <app_support>,
#subject = 'Update Trigger Error',
#body = #ERROR_MESSAGE
END CATCH
END
END
I think I have something, but would like some additional feedback, please:
CREATE TRIGGER OrderSplitting.UPDATE_Update_WCO_Status_tg
ON OrderSplitting.SplitOrdersHeader_tb
AFTER UPDATE AS
SET NOCOUNT ON
BEGIN TRY
INSERT INTO OrderSplitting.UpdateOtherSystem_tb(OriginalCustomerPONumber)
SELECT SOH.OriginalCustomerPONumber
FROM OrderSplitting.SplitOrdersHeader_tb SOH
INNER JOIN inserted I ON SOH.OriginalCustomerPONumber = I.OriginalCustomerPONumber
LEFT OUTER JOIN OrderSplitting.UpdateOtherSystem_tb UOS ON SOH.OriginalCustomerPONumber = UOS.OriginalCustomerPONumber
WHERE UOS.OriginalCustomerPONumber IS NULL
GROUP BY SOH.OriginalCustomerPONumber
HAVING COUNT(SOH.OriginalCustomerPONumber) = SUM(CAST(SOH.OrderConfirmationReceived as int))
END TRY
BEGIN CATCH
DECLARE #ERROR_MESSAGE varchar(MAX)
SET #ERROR_MESSAGE = ERROR_MESSAGE()
EXEC msdb..sp_send_dbmail
#recipients = <app_support>,
#subject = 'Update Trigger Error',
#body = #ERROR_MESSAGE
END CATCH

SQL Server - update multiple records using a stored procedure

Being a super novice at this, I would like some guidance on this, please.
I need to compare two sets of data and update one set with a value. This is what I have so far.
PROCEDURE [dbo].[update_personnel_rank]
AS
DECLARE #frsid VARCHAR
DECLARE #officerid VARCHAR
DECLARE #hrrank VARCHAR
DECLARE #personnelrank VARCHAR
DECLARE #farank VARCHAR
DECLARE #rank VARCHAR(150)
SET #rank = 'Admin Spec II'
BEGIN
SET NOCOUNT ON;
SELECT
#frsid = hr.FRSID,
#officerid = p.OfficerID,
#hrrank = hr.Rank,
#personnelrank = p.Rank,
#farank = r.FA_Rank
FROM
[FireApp_REPL_DW_Data].[dbo].[MCFRSCombinedPersonnelandPimsStaff] hr
INNER JOIN
[fh_reports].[dbo].[personnel_bk] p ON p.OfficerID = hr.FRSID
INNER JOIN
[fh_reports].[dbo].[Rank_Lookup_tbl] r ON r.FA_Rank = hr.Rank
WHERE
(p.rank <> hr.Rank OR p.rank = '')
AND p.Rank = #rank
UPDATE [fh_reports].[dbo].[personnel_bk]
SET Rank = #farank
WHERE OfficerID = #officerid
END
GO
The select query returns 3 records and this stored procedure runs without any error, but it does not update the records. Since the select query returns 3 records, I think I need to change the parameter setting accordingly, but not sure how...
To #Sami's point, if you are not returning those variables, you do not need to set them and can instead just run the update:
USE [YourDatabase]
GO
SET NOCOUNT ON
GO
ALTER PROCEDURE [dbo].[update_personnel_rank]
#rank VARCHAR(150) --= 'Admin Spec II'
AS
BEGIN
IF #rank IS NULL OR #rank = ''
RAISERROR('Please enter a valid rank string.', 16, 1)
UPDATE hr
SET [Rank] = r.FA_Rank
FROM [FireApp_REPL_DW_Data].[dbo].[MCFRSCombinedPersonnelandPimsStaff] [hr]
INNER JOIN [fh_reports].[dbo].[personnel_bk] [p]
ON [p].[OfficerID] = [hr].[FRSID]
INNER JOIN [fh_reports].[dbo].[Rank_Lookup_tbl] [r]
ON [r].[FA_Rank] = [hr].[Rank]
WHERE [p].[rank] <> [hr].[Rank]
AND ([p].[Rank] = #rank OR p.[Rank] = '')
END ;
GO

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.

How to detect interface break between stored procedure

I am working on a large project with a lot of stored procedures. I came into the following situation where a developer modified the arguments of a stored procedure which was called by another stored procedure.
Unfortunately, nothing prevents the ALTER PROC to complete.
Is there a way to perform those checks afterwards ?
What would be the guidelines to avoid getting into that kind of problems ?
Here is a sample code to reproduce this behavior :
CREATE PROC Test1 #arg1 int
AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
END
GO
CREATE PROC Test2 #arg1 int
AS
BEGIN
DECLARE #arg int;
SET #arg = #arg1+1;
EXEC Test1 #arg;
END
GO
EXEC Test2 1;
GO
ALTER PROC Test1 #arg1 int, #arg2 int AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
PRINT CONVERT(varchar(32), #arg2)
END
GO
EXEC Test2 1;
GO
DROP PROC Test2
DROP PROC Test1
GO
Sql server 2005 has a system view sys.sql_dependencies that tracks dependencies. Unfortunately, it's not all that reliable (For more info, see this answer). Oracle, however, is much better in that regard. So you could switch. There's also a 3rd party vendor, Redgate, who has Sql Dependency Tracker. Never tested it myself but there is a trial version available.
I have the same problem so I implemented my poor man's solution by creating a stored procedure that can search for strings in all the stored procedures and views in the current database. By searching on the name of the changed stored procedure I can (hopefully) find EXEC calls.
I used this on sql server 2000 and 2008 so it probably also works on 2005. (Note : #word1, #word2, etc must all be present but that can easily be changed in the last SELECT if you have different needs.)
CREATE PROCEDURE [dbo].[findWordsInStoredProceduresViews]
#word1 nvarchar(4000) = null,
#word2 nvarchar(4000) = null,
#word3 nvarchar(4000) = null,
#word4 nvarchar(4000) = null,
#word5 nvarchar(4000) = null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- create temp table
create table #temp
(
id int identity(1,1),
Proc_id INT,
Proc_Name SYSNAME,
Definition NTEXT
)
-- get the names of the procedures that meet our criteria
INSERT #temp(Proc_id, Proc_Name)
SELECT id, OBJECT_NAME(id)
FROM syscomments
WHERE OBJECTPROPERTY(id, 'IsProcedure') = 1 or
OBJECTPROPERTY(id, 'IsView') = 1
GROUP BY id, OBJECT_NAME(id)
-- initialize the NTEXT column so there is a pointer
UPDATE #temp SET Definition = ''
-- declare local variables
DECLARE
#txtPval binary(16),
#txtPidx INT,
#curText NVARCHAR(4000),
#counterId int,
#maxCounterId int,
#counterIdInner int,
#maxCounterIdInner int
-- set up a double while loop to get the data from syscomments
select #maxCounterId = max(id)
from #temp t
create table #tempInner
(
id int identity(1,1),
curName SYSNAME,
curtext ntext
)
set #counterId = 0
WHILE (#counterId < #maxCounterId)
BEGIN
set #counterId = #counterId + 1
insert into #tempInner(curName, curtext)
SELECT OBJECT_NAME(s.id), text
FROM syscomments s
INNER JOIN #temp t
ON s.id = t.Proc_id
WHERE t.id = #counterid
ORDER BY s.id, colid
select #maxCounterIdInner = max(id)
from #tempInner t
set #counterIdInner = 0
while (#counterIdInner < #maxCounterIdInner)
begin
set #counterIdInner = #counterIdInner + 1
-- get the pointer for the current procedure name / colid
SELECT #txtPval = TEXTPTR(Definition)
FROM #temp
WHERE id = #counterId
-- find out where to append the #temp table's value
SELECT #txtPidx = DATALENGTH(Definition)/2
FROM #temp
WHERE id = #counterId
select #curText = curtext
from #tempInner
where id = #counterIdInner
-- apply the append of the current 8KB chunk
UPDATETEXT #temp.definition #txtPval #txtPidx 0 #curtext
end
truncate table #tempInner
END
-- check our filter
SELECT Proc_Name, Definition
FROM #temp t
WHERE (#word1 is null or definition LIKE '%' + #word1 + '%') AND
(#word2 is null or definition LIKE '%' + #word2 + '%') AND
(#word3 is null or definition LIKE '%' + #word3 + '%') AND
(#word4 is null or definition LIKE '%' + #word4 + '%') AND
(#word5 is null or definition LIKE '%' + #word5 + '%')
ORDER BY Proc_Name
-- clean up
DROP TABLE #temp
DROP TABLE #tempInner
END
You can use sp_refreshsqlmodule to attempt to re-validate SPs (this also updates dependencies), but it won't validate this particular scenario with parameters at the caller level (it will validate things like invalid columns in tables and views).
http://www.mssqltips.com/tip.asp?tip=1294 has a number of techniques, including sp_depends
Dependency information is stored in the SQL Server metadata, including parameter columns/types for each SP and function, but it isn't obvious how to validate all the calls, but it is possible to locate them and inspect them.

Resources