I'm not from SQL background so I'll need a very clear answer. On daily basis I'm building interfaces in .net and sometimes pulling data from SQL using simple statements. I never had anything to do with the store procedures before. Both our developer(away on monthly vacation) and our DBA(who is sick for unforeseeable future) are away and being the only person left who at least saw SQL Server I ended up replacing both of them. I feel very much out of my depth and there is no one in my office to ask. My boss who used to be a SQL developer before he became boss and stopped doing technical things insisted today that there is a problem with this stored procedure and that I am to fix it. I truly have no idea how. C# is so much easier...
This (apparently faulty) stored procedure was designed to populate a new field - Sequence. It was meant to take a ClientID, and then update the User records for that ClientID. If the number of updated records varies from the expected number of updates, the transaction is supposed to rollback.
My boss claims that this procedure is written wrong. Could you please help and correct it for me so that I can use it with my limited SQL knowledge?
DECLARE #TargetRowCount INT
SELECT #TargetRowCount = COUNT(*)
FROM dbo.Users
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name)
BEGIN TRAN
UPDATE dbo.Users
SET Sequence = UPPER(Name)
SELECT COUNT(*)
FROM dbo.Users
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name)
IF(##RowCount <> #TargetRowCount)
ROLLBACK TRAN
ELSE
COMMIT TRAN
Thank you in advance!
i have a hard time to understad the purpose of that script but anyway.
see the comments i included. hopefully it'll help you:
DECLARE #TargetRowCount INT;
DECLARE #ChckRC1 INT;
DECLARE #ChckRC2 INT;
-- select how many rows with wrong name exist
SELECT #TargetRowCount = COUNT(*)
FROM dbo.Users
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name)
BEGIN TRAN
-- update users sequence, but really all?
UPDATE dbo.Users
SET Sequence = UPPER(Name)
-- i'd add the where clause from above
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name);
-- store how many rows the update hit
SET #ChckRC1 = ##ROWCOUNT;
-- select again how many rows with wrong name exist
SELECT #ChckRC2 = COUNT(*)
FROM dbo.Users
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name);
-- first count is different than actually updated rows,
-- some disappeared or new appeared.
IF (#ChckRC1 <> #TargetRowCount )
-- second count should be zero
-- any other value suggest, there is still a user with wrong sequence
OR (#ChckRC2 <> 0 )
ROLLBACK TRAN
ELSE
COMMIT TRAN
i have no idea why you are checking the counts 3 times. what i'd do is:
-- update users sequence of some client with wrong sequence
UPDATE dbo.Users
SET Sequence = UPPER(Name)
WHERE ClientId = #ClientId
AND Sequence <> UPPER(Name);
return ##ROWCOUNT;
Related
I have a table with x number of rows. I want to create a stored procedure that always select a new row and return that row (when all rows has been returned it will start over from first row). My idea is to select top 1 row (ordered by a date time row) return that from the stored procedure and then set an datetime column so next time it will be a new row that is returned. It needs to be thread safe so I would expect some row locking is needed (I don't know if this is true). How would you create a stored procedure like that? I am not sure of you need to use variables or it can be done in a single query. Something like:
select top 1 *
from [dbo].[AppRegistrations]
order by LastUsed
update [dbo].[AppRegistrations]
set LastUsed = getdate()
In the comments it is stated that it cannot be done in a single query. If I added following to a stored procedure will it then be thread safe? Or do I need to add a lock? And does the query make sense or should it be done differently?
declare #id int
declare #name as nvarchar(256)
select top 1 #id=id,#name=name from [dbo].[AppRegistrations] order by LastUsed
Update [dbo].[AppRegistrations] set LastUsed=getdate() where id=#id
select #id,#name
It is important that another query cannot interrupt returning a unique row because it updates a row between the select and the update. That is why I wanted it in a single query.
I tried to gather everything up and added a row lock. Following sample works as expected, but I dont know whether the row lock is the right way, or I should expect some challenges. Can someone validate if this approach is correct?
BEGIN TRAN
declare #id int
declare #name as nvarchar(256)
select top 1 #id=id,#name=name from [dbo].[AppRegistrations] WITH (HOLDLOCK, ROWLOCK) order by LastUsed
Update [dbo].[AppRegistrations] set LastUsed=getdate() where id=#id
select #id as id,#name as name
COMMIT TRAN
I make a good number of assumptions here
UPDATE [dbo].[AppRegistrations]
SET LastSelected = CURRENT_TIMESTAMP
OUTPUT INSERTED.*
WHERE Id = (SELECT TOP (1) Id
FROM [dbo].[AppRegistrations]
ORDER BY LastSelected
)
Here is some background on the OUTPUT https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?view=sql-server-ver15
Here is another reference where you can do slightly more complex things https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver15#CaptureResults
I have stored procedure in the sql server 2008, my stored procedure calculate and get the last number "not primary key" from column from table B and add one ( +1 ) to this number to use it on the next statement on the same stored procedure.
My issue that i have a duplicate number some times, i think this happened when multiple users call the stored procedure on the same time. is this the issue and how can i solve it
my code is like the below:-
DECLARE #ID AS NVARCHAR(10)
SET #ID = (
SELECT TOP 1 MyNo
FROM Employee
WHERE (
(TypeID = #TypeID) AND
(Year = #Year)
)
ORDER BY ID DESC
)
SET #ID = ISNULL(#ID,0) + 1
INSERT INTO Employee (name,lname,MyNo) VALUES (#name,#lname,#MyNo)
You can lock a table for the duration of a transaction with the WITH (TABLOCKX, HOLDLOCK) syntax:
BEGIN TRANSACTION
DECLARE #ID AS NVARCHAR(10)
SET #ID = (
SELECT TOP 1 MyNo
FROM Employee WITH (TABLOCKX, HOLDLOCK)
WHERE (
(TypeID = #TypeID) AND
(Year = #Year)
)
ORDER BY ID DESC
)
SET #ID = ISNULL(#ID,0) + 1
INSERT INTO Employee (name,lname,MyNo) VALUES (#name,#lname,#MyNo)
COMMIT TRANSACTION
You can find more information about TABLOCK and TABLOCKX here:
https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table
Per discussion, the best lock to use in this case would be:
(UPDLOCK,HOLDLOCK)
If you cannot use Identity column or the Table lock, another alternative is to use sp_getapplock
The advantage with this mechanism is that this kind of lock can be used across multiple stored procedures that should not run concurrently or for operations that span multiple tables. It also allows for handling timeout and other kinds of behavior if the lock is not available.
You have to be careful when using this feature and ensure you acquire and release locks properly or you will create more problems than you solve.
I was just trying to figure out how to do a basic trigger when I updated a row
Heres the setup
CREATE TABLE marriage(
personid int
married varchar(20)
);
INSERT INTO marriage
values (1, unmarried);
What im trying to do is create a sql trigger that will make it so that when I update a person can only go from married to divorced but not unmarried to divorced.
If anyone can help me with structuring this that would be great
This is what I was looking for if someone was looking for something similar
alter trigger
trigtest3
on married
for update
as
begin
declare #old varchar(20)
declare #new varchar(20)
select #old = married from deleted
select #new = married from inserted
if(#old like 'Unmarried' AND #new like 'Divorced')
rollback
end
SQL Server doesn't provide per-row triggers unfortunately, but only triggers for a complete command. And one single update command can update several rows, so you must look whether at least one affected row has undergone a forbidden change. You do this by joining the deleted and inserted pseudo tables on a column or a combination of columns that uniquely identify a record (i.e. the primary key).
create trigger trg_upd_married on marriage for update as
begin
declare #error_count int
select #error_count = count(*)
from deleted d
join inserted i on i.id = d.id
where d.married = 'Unmarried'
where i.married = 'Divorced'
if #error_count > 0
begin
raiserror('Unmarried persons cannot get divorced.', 16, 121)
rollback transaction
end
end;
The above trigger may still have errors. I am not fluent with TSQL (and just notice that I find its triggers quite clumsy - at least compared to Oracle's triggers I am used to).
You need to use instead of triggers as you need to prevent update. For update triggers are run after the insert happens. Use the following code -
create trigger abc on marriage
for instead of update
as
begin
Begin transaction
if exists(select 1 from deleted as a
inner join inserted as b
on a.personid = b.personid
where a.married = 'unmarried' and b.married = 'Divorced')
begin
raiserror('Status can not be changed from unmarried to Divorced',16,1)
Rollback transaction
end
else
begin
update a
set a.married = b.married
from marriage as a
inner join inserted as b
on a.personid = b.personid
Commit transaction
end
end
Let me know if this helps
Just want to get some views/possible leads on an issue I have.
I have a stored procedure that updates/deletes a record from a table in my database, the table it deletes from is a live table, that temporary holds the data, and also updates records on a archive table. (for reporting etc..) it works normally and havent had an issues.
However recently I had worked on a windows service to monitor our system (running 24/7), which uses a HTTP call to initiate a program, and once this program has finished it then runs the mention stored procedure to delete out redundant data. Basically the service just runs the program quickly to make sure its functioning correctly.
I have noticed recently that the data isnt always being deleted. Looking through logs I see no errors being reported. And Even see the record in the database has been updated correctly. But just doesnt get deleted.
This unfortunately has a knock on effect with the monitoring service, as this continously runs, and sends out alerts because the data cant be duplicated in the live table, hence why it needs to delete out the data.
Currently I have in place a procedure to clear out any old data. (3 hours).
Result has the value - Rejected.
Below is the stored procedure:
DECLARE #PostponeUntil DATETIME;
DECLARE #Attempts INT;
DECLARE #InitialTarget VARCHAR(8);
DECLARE #MaxAttempts INT;
DECLARE #APIDate DATETIME;
--UPDATE tCallbacks SET Result = #Result WHERE CallbackID = #CallbackID AND UPPER(Result) = 'PENDING';
UPDATE tCallbacks SET Result = #Result WHERE ID = (SELECT TOP 1 ID FROM tCallbacks WHERE CallbackID = #CallbackID ORDER BY ID DESC)
SELECT #InitialTarget = C.InitialTarget, #Attempts = LCB.Attempts, #MaxAttempts = C.CallAttempts
FROM tConfigurations C WITH (NOLOCK)
LEFT JOIN tLiveCallbacks LCB ON LCB.ID = #CallbackID
WHERE C.ID = LCB.ConfigurationID;
IF ((UPPER(#Result) <> 'SUCCESSFUL') AND (UPPER(#Result) <> 'MAXATTEMPTS') AND (UPPER(#Result) <> 'DESTBAR') AND (UPPER(#Result) <> 'REJECTED')) BEGIN
--INSERT A NEW RECORD FOR RTNR/BUSY/UNSUCCESSFUL/REJECT
--Create Callback Archive Record
SELECT #APIDate = CallbackRequestDate FROM tCallbacks WHERE Attempts = 0 AND CallbackID = #CallbackID;
BEGIN TRANSACTION
INSERT INTO tCallbacks (CallbackID, ConfigurationID, InitialTarget, Agent, AgentPresentedCLI, Callee, CalleePresentedCLI, CallbackRequestDate, Attempts, Result, CBRType, ExternalID, ASR, SessionID)
SELECT ID, ConfigurationID, #InitialTarget, Agent, AgentPresentedCLI, Callee, CalleePresentedCLI, #APIDate, #Attempts + 1, 'PENDING', CBRType, ExternalID, ASR, SessionID
FROM tLiveCallbacks
WHERE ID = #CallbackID;
UPDATE LCB
SET PostponeUntil = DATEADD(second, C.CallRetryPeriod, GETDATE()),
Pending = 0,
Attempts = #Attempts + 1
FROM tLiveCallbacks LCB
LEFT JOIN tConfigurations C ON C.ID = LCB.ConfigurationID
WHERE LCB.ID = #CallbackID;
COMMIT TRANSACTION
END
ELSE BEGIN
-- Update the Callbacks archive, when Successful or Max Attempts or DestBar.
IF EXISTS (SELECT ID FROM tLiveCallbacks WHERE ID = #CallbackID) BEGIN
BEGIN TRANSACTION
UPDATE tCallbacks
SET Attempts = #Attempts
WHERE ID IN (SELECT TOP (1) ID
FROM tCallbacks
WHERE CallbackID = #CallbackID
ORDER BY Attempts DESC);
-- The live callback should no longer be active now. As its either been answered or reach the max attempts.
DELETE FROM tLiveCallbacks WHERE ID = #CallbackID;
COMMIT
END
END
You need to fix your transaction processing. What is happening is that one statement is failing but since you don't have a try-catch block all changes are not getting rolled back only the statement that failed.
You should never have a begin tran without a try catch block and a rollback on error. I personally also prefer in something like this to put the errors and associated data into a table variable (which will not rollback) and then insert then to an exception table after the rollback. This way the data retains integrity and you can look up what the problem was.
I've got a table where I need to auto-assign an ID 99% of the time (the other 1% rules out using an identity column it seems). So I've got a stored procedure to get next ID along the following lines:
select #nextid = lastid+1 from last_auto_id
check next available id in the table...
update last_auto_id set lastid = #nextid
Where the check has to check if users have manually used the IDs and find the next unused ID.
It works fine when I call it serially, returning 1, 2, 3 ... What I need to do is provide some locking where multiple processes call this at the same time. Ideally, I just need it to exclusively lock the last_auto_id table around this code so that a second call must wait for the first to update the table before it can run it's select.
In Postgres, I can do something like 'LOCK TABLE last_auto_id;' to explicitly lock the table. Any ideas how to accomplish it in SQL Server?
Thanks in advance!
Following update increments your lastid by one and assigns this value to your local variable in a single transaction.
Edit
thanks to Dave and Mitch for pointing out isolation level problems with the original solution.
UPDATE last_auto_id WITH (READCOMMITTEDLOCK)
SET #nextid = lastid = lastid + 1
You guys have between you answered my question. I'm putting in my own reply to collate the working solution I've got into one post. The key seems to have been the transaction approach, with locking hints on the last_auto_id table. Setting the transaction isolation to serializable seemed to create deadlock problems.
Here's what I've got (edited to show the full code so hopefully I can get some further answers...):
DECLARE #Pointer AS INT
BEGIN TRANSACTION
-- Check what the next ID to use should be
SELECT #NextId = LastId + 1 FROM Last_Auto_Id WITH (TABLOCKX) WHERE Name = 'CustomerNo'
-- Now check if this next ID already exists in the database
IF EXISTS (SELECT CustomerNo FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo = #NextId)
BEGIN
-- The next ID already exists - we need to find the next lowest free ID
CREATE TABLE #idtbl ( IdNo int )
-- Into temp table, grab all numeric IDs higher than the current next ID
INSERT INTO #idtbl
SELECT CAST(CustomerNo AS INT) FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo >= #NextId
ORDER BY CAST(CustomerNo AS INT)
-- Join the table with itself, based on the right hand side of the join
-- being equal to the ID on the left hand side + 1. We're looking for
-- the lowest record where the right hand side is NULL (i.e. the ID is
-- unused)
SELECT #Pointer = MIN( t1.IdNo ) + 1 FROM #idtbl t1
LEFT OUTER JOIN #idtbl t2 ON t1.IdNo + 1 = t2.IdNo
WHERE t2.IdNo IS NULL
END
UPDATE Last_Auto_Id SET LastId = #NextId WHERE Name = 'CustomerNo'
COMMIT TRANSACTION
SELECT #NextId
This takes out an exclusive table lock at the start of the transaction, which then successfully queues up any further requests until after this request has updated the table and committed it's transaction.
I've written a bit of C code to hammer it with concurrent requests from half a dozen sessions and it's working perfectly.
However, I do have one worry which is the term locking 'hints' - does anyone know if SQLServer treats this as a definite instruction or just a hint (i.e. maybe it won't always obey it??)
How is this solution? No TABLE LOCK is required and works perfectly!!!
DECLARE #NextId INT
UPDATE Last_Auto_Id
SET #NextId = LastId = LastId + 1
WHERE Name = 'CustomerNo'
SELECT #NextId
Update statement always uses a lock to protect its update.
You might wanna consider deadlocks. This usually happens when multiple users use the stored procedure simultaneously. In order to avoid deadlock and make sure every query from the user will succeed you will need to do some handling during update failures and to do this you will need a try catch. This works on Sql Server 2005,2008 only.
DECLARE #Tries tinyint
SET #Tries = 1
WHILE #Tries <= 3
BEGIN
BEGIN TRANSACTION
BEGIN TRY
-- this line updates the last_auto_id
update last_auto_id set lastid = lastid+1
COMMIT
BREAK
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() as ErrorMessage
ROLLBACK
SET #Tries = #Tries + 1
CONTINUE
END CATCH
END
I prefer doing this using an identity field in a second table. If you make lastid identity then all you have to do is insert a row in that table and select #scope_identity to get your new value and you still have the concurrency safety of identity even though the id field in your main table is not identity.