what is the best way to avoid duplicate when there is more than one transaction at a time? - sql

I have a production issue basically when more than 1 user being approved at the same time. The company code for the users are the same. It is supposed to be unique. Even when i try to use begin tran and with no lock, there is still a probability that the community code can still be duplicated. Because the select statement will give the same amount and hold before updating the table. Is there any possible way i can counter this. Here is the script. Thank you.
declare #currentcompanycode as nvarchar(128)
begin tran
set #currentcompanycode = (select max(companycode) from tb_company with (nolock))
update tb_company set companycode = #currentcompanycode + 1 where companyid = #companyid
commit tran

Make company code an auto increment column and let the database do the work.

You need to hold on to a lock on the tb_company table when you first select it's MAX(companycode), until you've performed the UPDATE based on the value you selected:
set #currentcompanycode =
(select max(companycode) from tb_company with (HOLDLOCK, TABLOCKX))

use the OUTPUT clause
declare #newcompanycode table
(
companycode int
)
update tb_company
set companycode = companycode + 1
output inserted.companycode
into #newcompanycode
where . . .
select * from #newcompanycode

Related

Enumerate the multiple rows in a multi-update Trigger

I have something like the table below:
CREATE TABLE updates (
id INT PRIMARY KEY IDENTITY (1, 1),
name VARCHAR (50) NOT NULL,
updated DATETIME
);
And I'm updating it like so:
INSERT INTO updates (name, updated)
VALUES
('fred', '2020-11-11),
('fred', '2020-11-11'),
...
('bert', '2020-11-11');
I need to write an after update Trigger and enumerate all the name(s) that were added and add each one to another table but can't work out how enumerate each one.
EDIT: - thanks to those who pointed me in the right direction, I know very little SQL.
What I need to do is something like this
foreach name in inserted
look it up in another table and
retrieve a count of the updates a 'name' has done
add 1 to the count
and update it back into the other table
I can't get to my laptop at the moment, but presumably I can do something like:
BEGIN
SET #count = (SELECT UCount from OTHERTAB WHERE name = ins.name)
SET #count = #count + 1
UPDATE OTHERTAB SET UCount = #count WHERE name = ins.name
SELECT ins.name
FROM inserted ins;
END
and that would work for each name in the update?
Obviously I'll have to read up on set based SQL processing.
Thanks all for the help and pointers.
Based on your edits you would do something like the following... set based is a mindset, so you don't need to compute the count in advance (in fact you can't). It's not clear whether you are counting in the same table or another table - but I'm sure you can work it out.
Points:
Use the Inserted table to determine what rows to update
Use a sub-query to calculate the new value if its a second table, taking into account the possibility of null
If you are really using the same table, then this should work
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE(UCount,0) + 1
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
If however you are using a second table then this should work:
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE((SELECT UCount+1 from OTHERTAB T2 WHERE T2.[name] = OTHERTAB.[name]),0)
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
Using inserted and set-based approach(no need for loop):
CREATE TRIGGER trg
ON updates
AFTER INSERT
AS
BEGIN
INSERT INTO tab2(name)
SELECT name
FROM inserted;
END

Create a stored procedure that keeps returning new rows

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

How to compare TargetRowCount in Stored Procedure?

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;

SQLServer lock table during stored procedure

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.

SQL - Counting Returned Records

I'm building a stored procedure. This stored procedure needs to insert a record if a record with a specific value does not exist. If the value does exist, I need to update the record. The problem I'm having is determining if a record with the given value exists or not. I am using the following code:
DECLARE #record1ID as char(36)
SET #record1ID = (SELECT TOP 1 ID FROM Person WHERE [Role]='Manager')
DECLARE #record2ID as char(36)
SET #record2ID = (SELECT TOP 1 d.ID FROM Department d WHERE d.[ManagerID]=#record1ID)
-- If #record2ID is set update record, otherwise add record
-- how do I setup this if/else statement?
Thank you!
If this were a SQL Server as it looks like, you could do a count like this:
declare #rec_counter as int
set #rec_counter = 0
select #rec_counter = count(*) FROM Department d WHERE d.[ManagerID]=#record1
if (#rec_counter > 0)
begin
-- do whatever here
end
IF (EXISTS YOUR_SELECT)
BEGIN ...
or
IF (#record2ID IS NULL)
BEGIN ...
or use select count(*) instead of selecting a value