I came across this today and accident and was wondering a few things. The basic code setup is
Begin Transaction
Update Table
set column to value
Begin transaction
Update Table
set column to value
I have played with it a little and found you can not do a commit after a doing a rollback, but you can do a commit before a rollback, however the rollback negates the commit. I guess my question is, is there any purpose/use for this? I see not other then making my DBA slap me for bad code lol
The short answer is that the intent behind the design of nested transactions is to allow you to code reusable procedures (or blocks of code) where the 2 following situations can be handled automatically without having to write the code differently for both cases:
You can start and end a transaction if none has been started yet.
Or, if a transaction is already in progress, then you just participate in the on-going transaction.
So let's say you like to code all your reusable procedures in a transactional manner, like this (pseudo code):
create procedure Foo
begin transaction
perform DML 1
perform DML 2
perform DML 3
-- other stuff
commit transaction
end procedure
create procedure Blah
begin transaction
perform DML 1
perform DML 2
perform DML 3
-- other stuff
commit transaction
end procedure
But now, let's say that you now need the Blah procedure to incorporate what Foo does. Obviously, you wouldn't want to copy-paste the contents of Foo in Blah. A simple call to Foo makes more sense for reusability's sake, like this:
create procedure Blah
begin transaction
perform DML 1
perform DML 2
-- include a call to Foo here
Foo();
perform DML 3
-- other stuff
commit transaction
end procedure
In the above case, without any changes to Foo's code, the call to Blah will still behave as one big transaction, which is probably what you want.
It's exactly for cases like these that inner commits don't actually do anything. They really only serve the purpose of flagging that everything was ok up until that point. But the real commit only happens when the outer transaction commits everything.
Imagine if every commit actually committed the transaction, then, to ensure that you don't corrupt the outer transaction, you would have to add extra conditions at the beginning of every procedure to check if a transaction is already started, and only start one if none is found. So, every procedure would have to be coded something like this to ensure it's safe for calling within other procedures:
create procedure Foo
didIStartATransaction = false
if ##trancount = 0 then
begin transaction
didIStartATransaction = true
end if
perform DML 1
perform DML 2
perform DML 3
-- other stuff
if didIStartATransaction then
commit transaction
end if
end procedure
create procedure Blah
didIStartATransaction = false
if ##trancount = 0 then
begin transaction
didIStartATransaction = true
end if
perform DML 1
perform DML 2
perform DML 3
-- other stuff
if didIStartATransaction then
commit transaction
end if
end procedure
That said, nested transactions can still be dangerous if one of the procedures forgets to symmetrically start and commit a transaction.
And personally, I prefer to not have any transaction control statements in any of my procedures, and just have the calling code manage the transaction. I feel a lot safer that way.
Please take a look at SAVEPOINT syntax. This allows you to set points in a transaction you can rollback to.
https://msdn.microsoft.com/en-us/library/ms188378.aspx
Within there lies your answer :)
What you are making is a nested transation.
This is what's happening:
If you rollback an inner transaction then you rollback the outermost transaction too. On the other hand, if you commit an inner transaction your data modification doesn't happen until even then outermost transaction is committed.
More information here.
Obviously using a nested transaction in your code doesn't make sense at all. But think about this code:
BEGIN TRAN
UPDATE MyTable
SET Col1 = 'Val1';
EXEC dbo.SomeStoredProcedureUsingTransactions;
IF ##TRANCOUNT > 0
COMMIT TRAN;
Related
I need to provide an auto update feature to my application.
I am having problem in applying the SQL updates. I have the updated SQL statement in my .sql file and what i want to achieve is that if one statment fails then entire script file must be rolled back
Ex.
create procedure [dbo].[test1]
#P1 varchar(200),
#C1 int
as
begin
Select 1
end
GO
Insert into test (name) values ('vv')
Go
alter procedure [dbo].[test2]
#P1 varchar(200),
#C1 int
as
begin
Select 1
end
GO
Now in the above example, if i get the error in third statement of "alter procedure [dbo].[test2]" then i want to rollback the first two changes also which is creating SP of "test1" and inserting data into "test" table
How should i approach this task? Any help will be much appreciated.
If you need any more info then let me know
Normally, you would want to add a BEGIN TRAN at the beginning, remove the GO statements, and then handle the ROLLBACK TRAN/COMMIT TRAN with a TRY..CATCH block.
When dealing with DML though there are often statements that have to be at the start of a batch, so you can't wrap them in a TRY..CATCH block. In that case you need to put together a system that knows how to roll itself back.
A simple system would be just to backup the database at the start and restore it if anything fails (assuming that you are the only one accessing the database the whole time). Another method would be to log each batch that runs successfully and to have corresponding rollback scripts which you can run to put everything back should a later batch fail. This obviously requires much more work (writing an undo script for every script PLUS fully testing the rollbacks) and can also be a problem if people are still accessing the database while the upgrade is happening.
EDIT:
Here's an example of a simple TRY..CATCH block with transaction handling:
BEGIN TRY
BEGIN TRANSACTION
-- All of your code here, with `RAISERROR` used for any of your own error conditions
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
However, the TRY..CATCH block cannot span batches (maybe that's what I was thinking of when I said transactions couldn't), so in your case it would probably be something more like:
IF (OBJECT_ID('dbo.Error_Happened') IS NOT NULL)
DROP TABLE dbo.Error_Happened
GO
BEGIN TRANSACTION
<Some line of code>
IF (##ERROR <> 0)
CREATE TABLE dbo.Error_Happened (my_id INT)
IF (OBJECT_ID('dbo.Error_Happened') IS NOT NULL)
BEGIN
<Another line of code>
IF (##ERROR <> 0)
CREATE TABLE dbo.Error_Happened (my_id INT)
END
...
IF (OBJECT_ID('dbo.Error_Happened) IS NOT NULL)
BEGIN
ROLLBACK TRANSACTION
DROP TABLE dbo.Error_Happened
END
ELSE
COMMIT TRANSACTION
Unfortunately, because of the separate batches from the GO statements you can't use GOTO, you can't use the TRY..CATCH, and you can't persist a variable across the batches. This is why I used the very kludgy trick of creating a table to indicate an error.
A better way would be to simply have an error table and look for rows in it. Just keep in mind that your ROLLBACK will remove those rows at the end as well.
In SQL Server, how many transactions will this produce?
DECLARE #deleted BIGINT
SET #deleted = 100000
WHILE #deleted = 100000
BEGIN
DELETE TOP(100000) FROM MYTABLE WITH (ROWLOCK)
where Col1 = 7048 and COL2 = 39727 and Col3 = 0
SET #deleted = (SELECT ##ROWCOUNT)
END
If I cancel after running this for 10 minutes will it need to roll back?
Would adding a being transaction and end transaction fix this if I don't want it to rollback past one iteration after a cancel?
Would it make any difference if I put it in a stored procedure?
When you don't have the BEGIN TRANSACTION and COMMIT, you have implied transactions. And, each DELETE will be a separate transaction. So, if you cancel the script, it will rollback the current command. But, all previous DELETE steps are already committed.
If you add a BEGIN TRANSACTION before your code and a COMMIT after your code, then you get a single transaction. If you cancel the query, you leave an open transaction, where there is not commit or rollback. In this case, you must submit a ROLLBACK command to start the rollback process.
It will be an implicit transaction. remember ACID? everything in SQL Server is a transaction either implicit or explicit otherwise you wouldn't be able to guarantee ACID
I believe this will execute under a single transaction (which SQL Server creates for you in this case). You could run Profiler to validate this. Putting it in a stored proc will not make any difference. I might suggest you put a Begin Tran (and corresponding End Tran) for each pass through the loop. One thing this will help prevent is your transaction log getting too large.
i need to update a database where some of the table have changed (columns have been added). I want to perform this action in a proper transaction. If the code executes without any problem then i will commit the changes otherwise i will rollback the database back to its original state.
I want to do something like this:
BEGIN TRANSACTION
...Execute some sql statements here
COMMIT TRANSACTION (When every thing goes well)
ROLLBACK TRANSACTION (When something goes wrong)
Please tell me what is the best way to do this i know there is a ##TranCount variable but dont know its exact purpose.
Thanks.
Begin Transaction
Alter Table dbo.MyTable
Add Col1 varchar(50)
If ##Error = 0
Begin
Commit Transaction
End
Else
Begin
Rollback Transaction
End
##Error gets reset after each SQL Statement so you must check it immediately after each statement has been executed to check for any errors.
Say I have a stored procedure consisting of several separate SELECT, INSERT, UPDATE and DELETE statements. There is no explicit BEGIN TRANS / COMMIT TRANS / ROLLBACK TRANS logic.
How will SQL Server handle this stored procedure transaction-wise? Will there be an implicit connection for each statement? Or will there be one transaction for the stored procedure?
Also, how could I have found this out on my own using T-SQL and / or SQL Server Management Studio?
Thanks!
There will only be one connection, it is what is used to run the procedure, no matter how many SQL commands within the stored procedure.
since you have no explicit BEGIN TRANSACTION in the stored procedure, each statement will run on its own with no ability to rollback any changes if there is any error.
However, if you before you call the stored procedure you issue a BEGIN TRANSACTION, then all statements are grouped within a transaction and can either be COMMITted or ROLLBACKed following stored procedure execution.
From within the stored procedure, you can determine if you are running within a transaction by checking the value of the system variable ##TRANCOUNT (Transact-SQL). A zero means there is no transaction, anything else shows how many nested level of transactions you are in. Depending on your sql server version you could use XACT_STATE (Transact-SQL) too.
If you do the following:
BEGIN TRANSACTION
EXEC my_stored_procedure_with_5_statements_inside #Parma1
COMMIT
everything within the procedure is covered by the transaction, all 6 statements (the EXEC is a statement covered by the transaction, 1+5=6). If you do this:
BEGIN TRANSACTION
EXEC my_stored_procedure_with_5_statements_inside #Parma1
EXEC my_stored_procedure_with_5_statements_inside #Parma1
COMMIT
everything within the two procedure calls are covered by the transaction, all 12 statements (the 2 EXECs are both statement covered by the transaction, 1+5+1+5=12).
You can find out on your own by creating a small stored procedure that does something simple, say insert a record into a test table. Then Begin Tran; run sp_test; rollback; Is the new record there? If so, then the SP ignores the outside transaction. If not, then the SP is just another statement executed inside the transaction (which I am pretty sure is the case).
You must understand that a transaction is a state of the session. The session can be in an explicit transaction state because there is at least one BEGIN TRANSACTION that have been executed in the session wherever the command "BEGIN TRANSACTION" has been throwed (before entering in a routine or inside the routine code). Otherwise, the state of the session is in an implicit transaction state. You can have multiple BEGIN TRANSACTION, but only the first one change the behavior of the session... The others only increase the ##TRANCOUNT global sesion variable.
Implicit transaction state means that all SQL orders (DDL, DML and DCL comands) wil have an invisble integrated transaction scope.
I have a pattern that I almost always follow, where if I need to wrap up an operation in a transaction, I do this:
BEGIN TRANSACTION
SAVE TRANSACTION TX
-- Stuff
IF #error <> 0
ROLLBACK TRANSACTION TX
COMMIT TRANSACTION
That's served me well enough in the past, but after years of using this pattern (and copy-pasting the above code), I've suddenly discovered a flaw which comes as a complete shock.
Quite often, I'll have a stored procedure calling other stored procedures, all of which use this same pattern. What I've discovered (to my cost) is that because I'm using the same savepoint name everywhere, I can get into a situation where my outer transaction is partially committed - precisely the opposite of the atomicity that I'm trying to achieve.
I've put together an example that exhibits the problem. This is a single batch (no nested stored procs), and so it looks a little odd in that you probably wouldn't use the same savepoint name twice in the same batch, but my real-world scenario would be too confusing to post.
CREATE TABLE Test (test INTEGER NOT NULL)
BEGIN TRAN
SAVE TRAN TX
BEGIN TRAN
SAVE TRAN TX
INSERT INTO Test(test) VALUES (1)
COMMIT TRAN TX
BEGIN TRAN
SAVE TRAN TX
INSERT INTO Test(test) VALUES (2)
COMMIT TRAN TX
DELETE FROM Test
ROLLBACK TRAN TX
COMMIT TRAN TX
SELECT * FROM Test
DROP TABLE Test
When I execute this, it lists one record, with value "1". In other words, even though I rolled back my outer transaction, a record was added to the table.
What's happening is that the ROLLBACK TRANSACTION TX at the outer level is rolling back as far as the last SAVE TRANSACTION TX at the inner level. Now that I write this all out, I can see the logic behind it: the server is looking back through the log file, treating it as a linear stream of transactions; it doesn't understand the nesting/hierarchy implied by either the nesting of the transactions (or, in my real-world scenario, by the calls to other stored procedures).
So, clearly, I need to start using unique savepoint names instead of blindly using "TX" everywhere. But - and this is where I finally get to the point - is there a way to do this in a copy-pastable way so that I can still use the same code everywhere? Can I auto-generate the savepoint name on the fly somehow? Is there a convention or best-practice for doing this sort of thing?
It's not exactly hard to come up with a unique name every time you start a transaction (could base it off the SP name, or somesuch), but I do worry that eventually there would be a conflict - and you wouldn't know about it because rather than causing an error it just silently destroys your data... :-(
Agree with KM's solution.
I prefer to use GUIDs though to generate unique savepoint names.
DECLARE #savepoint AS VARCHAR(36)
SET #savepoint = CONVERT(VARCHAR(36), NEWID())
BEGIN TRANSACTION
SAVE TRANSACTION #savepoint
...
ROLLBACK TRANSACTION #savepoint
COMMIT TRANSACTION
look at the docs: SAVE TRANSACTION (Transact-SQL)
SAVE { TRAN | TRANSACTION } { savepoint_name | #savepoint_variable }
[ ; ]
looks like you can name it based on a variable, so try making your pattern:
DECALRE #savepoint_variable varchar(1000)
SET #savepoint_variable=OBJECT_NAME(##PROCID)+'|'+CONVERT(char(23),GETDATE(),121)
BEGIN TRANSACTION
SAVE TRANSACTION #savepoint_variable
-- Stuff
IF #error <> 0
BEGIN
ROLLBACK TRANSACTION #savepoint_variable
END
COMMIT TRANSACTION
when called from different procedures, your #savepoint_variable will have a different local value, and your rollbacks should rollback the proper. I put in the current datetime in the save point name, because you might use recursion at some point and if this is a copy paste pattern, it is better to handle all cases.