We have recently been parachuted to a new ETL project with very bad code.
I have in my hands a query with 700 rows and all sort of update.
I would like to debug it with SET XACT_ABORT ON; and the goal is to rollback everything if only one transaction fails.
But I find several way to archive it on StackOverflow like this:
BEGIN TRANSACTION;
BEGIN TRY
-- Multiple sql statements goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH
or this:
BEGIN TRY
BEGIN TRANSACTION
-- Multiple sql statements goes here
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT(ERROR_MESSAGE())
ROLLBACK TRANSACTION
END CATCH
and none of these uses SET XACT_ABORT ON;.
I don't understand, is SET XACT_ABORT ON the same as using BEGIN TRY BEGIN TRANSACTION?
Can I just use:
SET XACT_ABORT ON;
-- Multiple sql statements goes here
and get ridof all the:
BEGIN TRANSACTION;
BEGIN TRY
?
And also, should I use BEGIN TRANSACTION and then BEGIN TRY or the other way around?
It is not the same. It decides when errors are thrown.
You should always use SET XACT_ABORT ON, because it is more consistent; almost always, an error will stop execution and throw an error. Else, half things throw errors and the other half continue execution.
There is a great article about this whole subject on Erland Sommarskog's site, and if you go at this point you will see a table which describes this strange behaviour. In conclusion, I recommend the General Pattern for Error Handling, as it is very well documented as well as provide you the opportunity to tweak it according to its own documentation.
Thank you for the resources #George Menoutis.
I post here my practical solution:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- Multiple sql statements goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO
SET XACT_ABORT OFF;
If you have XACT_ABORT ON there is no need to manually catch any errors, unless you are doing error logging. XACT_ABORT will cause all errors to doom the transaction and roll it back.
All you need is
SET XACT_ABORT ON;
BEGIN TRAN;
--do stuff
COMMIT;
If there is only one statement then you don't even need BEGIN TRAN; and COMMIT;
Related
I have this statement block with transaction and try/catch:
begin try
begin transaction;
insert ....
update ....
delete ....
commit transaction;
end try
begin catch
if (##trancount > 0)
rollback transaction;
throw;
end catch;
I am using ##trancount here without fully understanding what happens.
Why exactly could it ever happen that ##trancount is zero in this case?
The most common scenario to receive ##trancount = 0 requires a bit more elaborated logic than your sample uses.
If you have other stored procedures called in the try block, they might have their own understanding of how transactions should be managed, and either due to a poorly written code, or some other mishap, they can either commit the outer transaction by accident, or roll it back (remember, there is no really such thing in SQL Server as a nested transaction, so any rollback statement that doesn't reference a previously declared savepoint wipes out everything). The error in this case might vary, either the one that caused the inner procedure to misbehave in the first place, or if nothing else, you'll get error 266, "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = %ld, current count = %ld."
Note that this behaviour can also be caused by rollback done by a trigger, as well.
It is possible that there might be some other situations when you end up with no current transaction in the catch block, but apparently they are so rare that I can't think of anything else off the top of my head.
Personally, I use the following template for all my stored procedures whenever possible:
create procedure dbo.ProcTemplate
(
#Error int = null output,
#Message nvarchar(2048) = null output
) as
/*
20191223, RW - to be completed
*/
set nocount, quoted_identifier, ansi_nulls, ansi_warnings, ansi_padding, concat_null_yields_null, arithabort on;
set xact_abort, implicit_transactions, numeric_roundabort off;
declare #XTran bit = cast(sign(##trancount) as bit);
begin try
if #XTran = 0
begin tran;
-- Put your code here
if #XTran = 0
commit;
end try
begin catch
if nullif(#Error, 0) is null
select #Error = error_number(), #Message = error_message();
if ##trancount > 0 and #XTran = 0
rollback;
end catch;
return;
go
One can argue that explicitly issuing set xact_abort off might result in some unpleasant side effects, such as a batch-terminating error (208, for instance) skips the catch and leaves the current transaction open. That's up to you; the trade-offs here are:
Better diagnostic. When all stored procs in a database follow this template, they bubble up the error to the outermost procedure by the means of output parameters and gracefully rollback everything.
Possibility to continue execution after the error. For example, log the error after the transaction has been rolled back and make sure the log record won't disappear with the rest of transaction.
Sorry for the horrid title...it's not very easy to explain what I'm asking in one line.
I want to run two dependent DML queries in a loop...if either of the queries fail/throw an error...I want to rollback the transaction, exit the loop, and terminate the entire proc while throwing an error (so jobs will detect the failure). I think what I have is correct...but I have a few questions because I want to better understand how it works. I've read the Microsoft documentation...but I'm still unclear on some things.
I know by using SET XACT_ABORT ON; that it will handle the rollback for the tran. Does that mean I do not need to check for IF (##TRANCOUNT > 0) in the CATCH block?
The other question...the reason I have the TRY...CATCH block is because of the WHILE loop...I'm not sure if the failed transaction will also terminate the proc, so I'm forcing it with THROW?
Here's what I have: (Ignore the fact that it's an infinite loop, I'm not including the break logic to keep the example simple)
SET XACT_ABORT ON;
WHILE(1=1)
BEGIN
BEGIN TRY
BEGIN TRAN;
--DML Query 1
--DML Query 2
COMMIT TRAN;
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
ROLLBACK TRAN;
THROW;
END CATCH
END
UPDATE
Okay I'm trying to figure out how to test it myself, I was having a hard time figuring out how to test it, but I think I've got it now. So now I feel bad for posting the question :D
Here's the test I've got so far...I'll update it as I make changes to it. It appears that the IF (##TRANCOUNT > 0) in the CATCH block is not necessary, because when I remove the ROLLBACK and check for a transaction after a failure...there's no open transaction. However, if I leave it in...the IF statement resolves to true and it still runs the rollback without error.
SET NOCOUNT ON;
IF OBJECT_ID('dbo.ChadTestTable1') IS NOT NULL DROP TABLE dbo.ChadTestTable1; --SELECT * FROM dbo.ChadTestTable1
CREATE TABLE dbo.ChadTestTable1 (TestField VARCHAR(10) NOT NULL)
IF OBJECT_ID('dbo.ChadTestTable2') IS NOT NULL DROP TABLE dbo.ChadTestTable2; --SELECT * FROM dbo.ChadTestTable2
CREATE TABLE dbo.ChadTestTable2 (TestField VARCHAR(10) NOT NULL)
INSERT INTO dbo.ChadTestTable1 (TestField) VALUES ('Test1')
INSERT INTO dbo.ChadTestTable2 (TestField) VALUES ('Test1')
SET XACT_ABORT ON;
WHILE(1=1)
BEGIN
BEGIN TRY
BEGIN TRAN;
RAISERROR('Update first table',0,1) WITH NOWAIT;
UPDATE dbo.ChadTestTable1 SET TestField = 'Test3'
RAISERROR('Update second table',0,1) WITH NOWAIT;
UPDATE dbo.ChadTestTable2 SET TestField = NULL
RAISERROR('Updates done',0,1) WITH NOWAIT;
COMMIT TRAN;
END TRY
BEGIN CATCH
--It appears this isn't necessary...but if it's here, it still resolves to true and runs?
IF (##TRANCOUNT > 0)
BEGIN
RAISERROR('Rolling back transaction',0,1) WITH NOWAIT;
ROLLBACK TRAN;
END
RAISERROR('Throwing Error',0,1) WITH NOWAIT;
THROW;
END CATCH
RAISERROR('End of loop',0,1) WITH NOWAIT;
BREAK;
END
SELECT * FROM dbo.ChadTestTable1 ctt
SELECT * FROM dbo.ChadTestTable2 ctt
So I figured out my own answer. I was having a fairly difficult time trying to figure out how to test this myself which is why I posted up the question, but I eventually figured it out. Here's what I found...90% of it you can probably find in Microsoft Documentation, but I read that, and was still left confused, so I resulted to bruteforce testing.
Here's what I found that I was missing after reading the documentation:
XACT_ABORT ON - Not only does it rollback the transaction, but it will also throw a terminating error...aka, if it's on and a transaction is in a loop, and an error is thrown in the tran, it will rollback the tran, break the loop, and it will also end the proc and nothing after it will be run. The proc/script ends there.
Therefore...I don't need the TRY...CATCH block in order to run the THROW and I don't need to rollback because it will be done automatically. I'm happy to have found this because that simplifies my code quite a bit.
I am seeing some strange behavior which has occurred on random basis.
Here's what my store procedure basically does.
begin try
begin tran
insert into table1
update table2
insert into table3
commit tran
end try
begin catch
rollback tran
end catch
For most of the time above code works fine except once in a while(once per day or two) when some error occurs, the transaction does not rollback the changes from all 3 tables.
begin try
begin tran
insert into table1----Rollback doesn't happen
update table2--Rollback happens
insert into table3--Rollback happens
commit tran
end try
begin catch
rollback tran
end catch
Can anyone please can suggest something where I might be wrong or do I need to handle transaction in some another way?
Thanks in advance.
Check your SET XACT_ABORT setting and what the error level inside the failing sql actually is
How to make SET XACT_ABORT ON rollback the transaction?
I used to use this code snippet within my stored procedure in SQL Server:
create procedure proc_name
--declare variables
as
set nocount on
begin transaction
begin try
--do something
commit transaction
end try begin catch
rollback transaction
;throw
end catch
go
but today I got to know 'set xact_abort on' statement.
Is the following code equivalent to previous one? Are there any differences between them?
create procedure proc_name
--declare variables
as
set nocount on
set xact_abort on
begin transaction
--do something
commit transaction
go
Quoting from MS docs
A TRY…CATCH construct catches all execution errors that have a severity higher than 10 that do not close the database connection.
So, try catch does not catch all possible errors. You can use xact_abort on in addition to try catch.
try/catch give you more flexibility, i.e., you are not limited to just a rollback when something is not happy.
If I simply wrap my query with:
BEGIN TRANSACTION
COMMIT TRANSACTION
If anything fails inside of that, will it automatically rollback?
From looking at other code, they seem to check for an error, if there is an error then they do a GOTO statement which then calls ROLLBACK TRANSACTION
But that seems like allot of work, to have to check for IF( ##ERROR <> 0) after every insert/update.
I typically do something like this inside my stored procedures. It keeps things nice and safe and passes along any errors that I encounter.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- Code goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
DECLARE
#ERROR_SEVERITY INT,
#ERROR_STATE INT,
#ERROR_NUMBER INT,
#ERROR_LINE INT,
#ERROR_MESSAGE NVARCHAR(4000);
SELECT
#ERROR_SEVERITY = ERROR_SEVERITY(),
#ERROR_STATE = ERROR_STATE(),
#ERROR_NUMBER = ERROR_NUMBER(),
#ERROR_LINE = ERROR_LINE(),
#ERROR_MESSAGE = ERROR_MESSAGE();
RAISERROR('Msg %d, Line %d, :%s',
#ERROR_SEVERITY,
#ERROR_STATE,
#ERROR_NUMBER,
#ERROR_LINE,
#ERROR_MESSAGE);
END CATCH
yes it is important to explicitly rollback the transaction in the case that it does not work.
I usually tell my son you only have to brush the teeth you want to keep.
In this case, you only need to rollback the commands you don't want to execute.
This will automatically rollback the transaction in case off error
SET XACT_ABORT ON
BEGIN TRANSACTION
-- CODE HERE
COMMIT TRANSACTION
For transaction control you use begin, commit and rollback. You begin a transaction by supplying BEGIN TRANSACTION. Then you put the various SQL statements you need. Then you end the transaction by issuing either a commit or rollback. COMMIT TRANSACTION will commit all the changes that you did to the database after the BEGIN statement and make them permanent, so to speak. ROLLBACK TRANSACTION will rollback all changes that you did to the database after the BEGIN statement. However, it will not change variable values.
Example:
BEGIN TRANSACTION
UPDATE table SET column = 'ABC' WHERE column = '123'
COMMIT TRANSACTION
--//column now has a value of 'ABC'
BEGIN TRANSACTION
UPDATE table SET column = 'ABC' WHERE column = '123'
ROLLBACK TRANSACTION
--//column still has it's previous value ('123') No changes were made.