SQL Delete with Transaction - sql

I have a table that has id, name, age. (Table name : Employee)
The id is the primary key in the table. There is an Sproc which deletes the entry given the name.
So in my sproc for deletion, I first select the id based on the name. If the name is valid then I do a delete.
DECLARE #Id uniqueidentifier
BEGIN TRANSACTION
SELECT TOP 1 #Id=Id FROM Employee WHERE Name=#Name
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RETURN ERROR_NOT_FOUND
END
DELETE FROM EMPLOYEE WHERE Id = #Id
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RETURN ERROR_NOT_FOUND
END
COMMIT TRANSACTION
My question is whether I need to need a transaction in this case or not. I understand that we need a transaction when we want atomic operations (set of operations should pass/fail together).
Please comment for the above scenario whether a transaction is required.. and what are the pros / cons of with / without transaction.

As for the answer to your question - if there is no employee with this name neither delete nor select will not change anything in the database, thus rolling back is not necessary.
Now if id is unique and the name is not - selecting an employee by the name is pretty dangerous since you have no real control which employee (from ones with the same name) you are going to delete. Looks like this procedure should take id as a parameter, rather than selecting it by name.

In your example code both ROLLBACK statements aren't actually rolling anything back since in both cases you haven't changed anything in the database. In other words, no, they aren't necessary.

Related

Using BizTalk 2013r2 to UPSERT via WCF-SQL stored procedure

I'm currently trying to write a canonical schema to multiple related tables within a SQL DB, but I'm experience DUPLICATE KEY ID conflicts when it's evaluating whether the record exists prior to UPDATING/INSERTING.
BizTalk receives change records from the student management system every 5 minutes, maps them to a stored procedure and then calls that procedure which writes the changes to 5 tables in our master database.
I believe this is because I'm using an incorrect design pattern in the stored procedure.
Current Design:
IF EXISTS (Select student_id FROM student_modules WHERE student_id #student_id and module_id = #module_id)
-- THEN UPDATE THE RECORD
ELSE
-- INSERT THE RECORD
Logically this makes sense, but as BizTalk receives 2 change records with the exact same student and module ID at the same time, and then attempts to call the stored procedure for each record.
SQL then panics, because whilst it's evaluating the logic in the first message, it tries to execute the INSERT whilst evaluating the same logic in the second message - and tells me I'm trying to insert a DUPLICATE KEY.
I've tried using an UPSERT pattern that i found at the below link (design below), but that seems to lock the student_modules table completely.
BEGIN TRANSACTION;
UPDATE dbo.t WITH (UPDLOCK, SERIALIZABLE) SET val = #val WHERE [key] = #key;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.t([key], val) VALUES(#key, #val);
END
COMMIT TRANSACTION;
https://sqlperformance.com/2020/09/locking/upsert-anti-pattern
Is there a cleaner approach to this that I'm missing?
You could use the MERGE Transact-SQL command
INSERT tbl_A (col, col2)
SELECT col, col2
FROM tbl_B
WHERE NOT EXISTS (SELECT col FROM tbl_A A2 WHERE A2.col = tbl_B.col);
You will also want to consider either changing your Orchestration so that it subscribes to further updates for the same student ID (a singleton type pattern) or to set your send port to ordered delivery, to prevent trying to update the same record at the same time.

Could a SELECT inside of a transaction lock the table?

I would like to know if it's possible that a select is blocking a table if it's inside a transaction.
It's something like this:
CREATE PROCEDURE InsertClient (#name NVARCHAR(256))
AS
BEGIN
DECLARE #id INT = 0;
BEGIN TRY
BEGIN TRAN InsertingClient
SELECT #id = MAX(ID) + 1 FROM Clients;
INSERT INTO Clients (Id, Name)
VALUES (#id, #name);
SELECT id, name
FROM Clients;
COMMIT TRAN InsertingClient
END TRY
BEGIN CATCH
ROLLBACK TRAN InsertingClient
END CATCH;
END
It's a dummy example, but if there's a lot of records in that table, and an API is receiving a lot of requests and calling this stored procedure, could be blocked by the initial and final select? Should I use the begin and commit only in the insert to avoid the block?
Thanks!
Based on the sample code you have provided it is critical that the first select is within the transaction because it appears you are manually creating an id based on the max id in the table, and without locking the table you could end up with duplicates. One assumes your actual code has some locking hints (e.g. with (updlock,holdlock)) to ensure that.
However your second select should not be in your transaction because all it will serve to do is make the locks acquired earlier in the transaction last the additional time of the select, when (again based on the sample code) there is no need to do that.
As an aside there are much better ways to generate an id such as using an identity column.

Make sure only one record inserted in table with thousands of concurrent users

Recently, I needed to write a stored procedure to insert only one record when the first user come and ignore for others. I think the IF NOT EXISTS INSERT will not work for me. Also, some people saying online that MERGE adds race condition. Any quick way to achieve this? This is my code for now.
IF NOT EXISTS (SELECT ......)
INSERT
You might add another table to use as the lock mechanism.
Let's say your table's name is a, and the name of the table which has the locked value is check_a :
create table a (name varchar(10))
create table check_a (name varchar(10))
Insert only one record to the lock table:
insert into check_a values ('lock')
go
Then create a stored procedure which checks if there is a value in the main table. If there is no record, we might lock the only value in the table check_a and insert our value into the table a.
create proc insert_if_first
as
begin
set nocount on
if not exists (select name from a)
begin
declare #name varchar(10)
begin tran
select #name = name from check_a with (updlock)
if not exists (select name from a)
begin
insert into a values ('some value')
end
commit
end
end
go
First selection from the table a to check there is no record is for using system resources as low as we can. If there is a record in the table a, we can skip opening transaction and skip locking the row.
Second check is to make sure that while we are waiting to obtain the lock, no one inserted a row to the table a.
This way, only the first user which can lock check_a will be able to insert a value to the table a.
I'm guessing that you mean you want users to make a stored procedure that makes sure only one user can run the procedure. Then you need to use isolation levels. There are different Isolation levels, so you need to decide which one you need.
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
You can read what they do here:
https://msdn.microsoft.com/en-us/library/ms173763.aspx

SQL Server : delete row if no constraint error

Much more complicated then this, but this is the basic
Person table (id, name, emailaddress)
Salesperson table (id, personID)
CustomerServiceRep table (id, personID)
Jeff is salesperson (id=4) and customerservicerep (id=5) with personID=1.
Simple
Trigger on SalesPerson Table
AFTER DELETE
AS
DECLARE #personID int = (SELECT personID FROM deleted);
IF #personID IS NOT NULL
BEGIN TRY
DELETE FROM Person
WHERE Person.id = #personID;
END TRY
BEGIN CATCH
END CATCH
DELETE FROM SalesPerson WHERE id=4;
Causes
Msg 3616, Level 16, State 1
An error was raised during trigger execution. The batch has been aborted and the user transaction, if any, has been rolled back.
I'm sure there's a much simpler way to not delete personID if it exists from some kind of constraint. Or catch the constraint. To go through every possible table that this could be in seems very repetitive and potentially more difficult when there are more tables/columns that may use this same table/constraint (foreign key).
You need an instead of delete trigger here rather than an after trigger.
CREATE Trigger tr_Delete_person
on Person
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Delete any child records
Delete FROM SalesPerson
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = SalesPerson.personID)
Delete FROM CustomerServiceRep
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = CustomerServiceRep .personID)
-- Finally delete from the person table
DELETE FROM Person
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = Person .personID)
END
You also have a fundamental flaw in your trigger in that you seem to expect that the trigger will be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Deleted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
DECLARE #personID int = (SELECT personID FROM deleted);
It's undefined - you'll get the value from one, arbitrary row in Deleted, and all others are ignored - typically not what you want!
You need to rewrite your entire trigger with the knowledge the Deleted WILL contain multiple rows! You need to work with set-based operations - don't expect just a single row in Deleted !

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.