Transactions are an area that I often find are used badly, or not used at all. Transactions without any error handling, nested transactions, named transactions named for no good reason or just plain lack of transactions where they should be used.
This is going to be the first of three posts looking at some aspects of transactions and transaction management. And deserving of the first post is that most misunderstood of things, the nested transaction.
Before getting into the details, I need to make one thing clear. Nested transactions are a lie. They do not exist in SQL Server.
A piece of code may appear to have multiple nested transactions in it. It may have multiple BEGIN TRANSACTION statement in it. It only has one transaction though.
An easy way to show this would be to look at the transaction log. Since transactions are there to make multiple statements atomic (committed or rolled back as a unit), the transactions must be reflected in the transaction log to allow SQL to know where to roll things back to or what statements need to be undone/redone as a unit.
CREATE TABLE TestingTransactions ( SomeNumber INT, SomeString VARCHAR(50) ); GO INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) VALUES (0, 'Zero'); CHECKPOINT; -- mark the log as reusable BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) VALUES (1, 'One'); BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) VALUES (2, 'Two'); BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) VALUES (3, 'Three'); COMMIT TRANSACTION COMMIT TRANSACTION COMMIT TRANSACTION SELECT Operation, Context, [Transaction ID], AllocUnitName, [Transaction Name] FROM fn_dblog(NULL, NULL); GO
I’m inserting a row before the transaction because allocating the first page to a table generates a lot of log entries. Those log records are not relevant to the subject at hand and will just confuse things.
One table, three transactions, three inserts, three commits. Let’s see what went into the transaction log.
What went into the log was a single begin transaction, a single commit and three inserts. The other two begin transaction statements, and the other two commit transaction statements were completely ignored for the purposes of the transaction log.
The other two begin transaction statements did do something. Not very much, and nothing that reflected in the transaction log, but they did do one thing, and if I make the code a little more complicated it’ll become clear what that one thing is.
CHECKPOINT; -- mark the log as reusable BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (1, 'One'); SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran1; BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (2, 'Two'); SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran2; BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (3, 'Three'); SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran3; COMMIT TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterCommitTran1; COMMIT TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterCommitTran2; COMMIT TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterCommitTran3;
Each BEGIN TRANSACTION incremented the value returned by @@TranCount, each COMMIT TRANSACTION decremented the value returned by @@TranCount. If I looked at the transaction log after each statement, the BEGIN_XACT log entry corresponds to the BEGIN TRANSACTION which incremented @@Trancount from 0 to 1, and the COMMIT_XACT log entry corresponds to the COMMIT TRANSACTION which decremented @@Trancount from 1 to 0.
Only the operations which changed @@Trancount to or from 0 are reflected in the transaction log, they’re the only ones which have any effect on what becomes part of the database’s durable state.
Does ROLLBACK then follow the same rules? Does it get ignored unless it’s the last statement of multiple COMMIT/ROLLBACK statements?
CHECKPOINT; -- mark the log as reusable BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (1, 'One'); SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran1; BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (2, 'Two') SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran2; BEGIN TRANSACTION INSERT INTO dbo.TestingTransactions (SomeNumber, SomeString) OUTPUT inserted.* VALUES (3, 'Three') SELECT @@TRANCOUNT AS TransactionCountAfterBeginTran3; ROLLBACK TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterRollbackTran1; COMMIT TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterCommitTran2; COMMIT TRANSACTION SELECT @@TRANCOUNT AS TransactionCountAfterCommitTran3; SELECT Operation, Context, [Transaction ID], AllocUnitName, [Transaction Name] FROM fn_dblog(NULL, NULL);
No. The ROLLBACK TRANSACTION set the value of @@Trancount directly to 0 and undid all of the inserts back to the outermost BEGIN TRANSACTION. The COMMIT TRANSACTION statements which ran after it all threw errors
Msg 3902, Level 16, State 1, Line 39
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
This is the danger of nesting transactions, that one ROLLBACK, anywhere after the first BEGIN TRANSACTION will undo everything right the way back to that first BEGIN TRANSACTION. These are not autonomous transactions being run one inside another, there’s a single transaction and rollback anywhere will roll back everything.
This can be a debugging nightmare when the application creates a transaction and calls a procedure which begins a transaction which calls other procedures which begin and commit transactions. If something goes wrong in one procedure and that procedure rolls the transaction back, it can end up undoing a lot more work than the developer of that procedure may have expected.
In general I would recommend only starting and committing transactions at a single level, and making transaction handling part of the application’s design, not something that falls out of the implementation by accident. If there’s an intentional design around how transactions will be handled (and choosing not to have any is not a good idea), then it’s a lot less likely that there will be transaction-related bugs that need to be found and fixed.
And what happens when working with an existing app where this wasn’t done? I’ll look at that in the next post on transactions, on conditionally starting transactions and on the use of savepoints.
The full list is:
- A Mess of Nested Transactions (This post)
- Savepoints and conditional transactions
- Why would you want to name a transaction?
- When naming transactions causes an error