(Look in the Misconceptions blog category for the rest of the month’s posts and check out the 60-page PDF with all the myths and misconceptions blog posts collected together when you join our Insider list, plus my online Myths and Misconceptions training course on Pluralsight.)
Nested transactions are an evil invention designed to allow developers to make DBAs’ lives miserable. In SQL Server, they are even more evil…
Myth #26: nested transactions are real in SQL Server.
Nested transactions do not actually behave the way the syntax would have you believe. I have no idea why they were coded this way in SQL Server – all I can think of is someone from the dim and distant past is continually thumbing their nose at the SQL Server community and going “ha – fooled you!!”.
Let me explain. SQL Server allows you to start transactions inside other transactions – called nested transactions. It allows you to commit them and to roll them back.
The commit of a nested transaction has absolutely no effect – as the only transaction that really exists as far as SQL Server is concerned is the outer one. Can you say ‘uncontrolled transaction log growth’? Nested transactions are a common cause of transaction log growth problems because the developer thinks that all the work is being done in the inner transactions so there’s no problem.
The rollback of a nested transaction rolls back the entire set of transactions – as there is no such thing as a nested transaction.
Your developers should not use nested transactions. They are evil.
If you don’t believe me, here’s some code to show you what I mean. First off – create a database with a table that each insert will cause 8KB in the log.
CREATE DATABASE [NestedXactsAreNotReal]; GO USE [NestedXactsAreNotReal]; GO ALTER DATABASE [NestedXactsAreNotReal] SET RECOVERY SIMPLE; GO CREATE TABLE [t1] ([c1] INT IDENTITY, [c2] CHAR (8000) DEFAULT 'a'); CREATE CLUSTERED INDEX [t1c1] ON [t1] ([c1]); GO SET NOCOUNT ON; GO
Test #1: Does rolling back a nested transaction only roll back that nested transaction?
BEGIN TRAN OuterTran; GO INSERT INTO [t1] DEFAULT VALUES; GO 1000 BEGIN TRAN InnerTran; GO INSERT INTO [t1] DEFAULT Values; GO 1000 SELECT @@TRANCOUNT, COUNT (*) FROM [t1]; GO
I get back the results 2 and 2000. Now I’ll roll back the nested transaction and it should only roll back the 1000 rows inserted by the inner transaction…
ROLLBACK TRAN InnerTran; GO
Msg 6401, Level 16, State 1, Line 1 Cannot roll back InnerTran. No transaction or savepoint of that name was found.
Hmm… from Books Online, I can only use the name of the outer transaction or no name. I’ll try no name:
ROLLBACK TRAN; GO SELECT @@TRANCOUNT, COUNT (*) FROM [t1]; GO
And I get the results 0 and 0. As Books Online explains, ROLLBACK TRAN rolls back to the start of the outer transaction and sets @@TRANCOUNT to 0. All changes are rolled back. The only way to do what I want is to use SAVE TRAN and ROLLBACK TRAN to the savepoint name.
Test #2: Does committing a nested transaction really commit the changes made?
BEGIN TRAN OuterTran; GO BEGIN TRAN InnerTran; GO INSERT INTO [t1] DEFAULT Values; GO 1000 COMMIT TRAN InnerTran; GO SELECT COUNT (*) FROM [t1]; GO
I get the result 1000, as expected. Now I’ll roll back the outer transaction and all the work done by the inner transaction should be preserved…
ROLLBACK TRAN OuterTran; GO SELECT COUNT (*) FROM [t1]; GO
And I get back the result 0. Oops – committing the nested transaction did not make its changes durable.
Test #3: Does committing a nested transaction at least let me clear the log?
I recreated the database again before running this so the log was minimally sized to begin with, and the output from DBCC SQLPERF below has been edited to only include the NestedXactsAreNotReal database.
BEGIN TRAN OuterTran; GO BEGIN TRAN InnerTran; GO INSERT INTO [t1] DEFAULT Values; GO 1000 DBCC SQLPERF ('LOGSPACE'); GO
Database Name Log Size (MB) Log Space Used (%) Status --------------------- ------------- ------------------ ----------- NestedXactsAreNotReal 12.05469 95.81983 0
Now I’ll commit the nested transaction, run a checkpoint (which will clear all possible transaction log in the SIMPLE recovery model), and check the log space again:
COMMIT TRAN InnerTran; GO CHECKPOINT; GO DBCC SQLPERF ('LOGSPACE'); GO
Database Name Log Size (MB) Log Space Used (%) Status --------------------- ------------- ------------------ ----------- NestedXactsAreNotReal 12.05469 96.25324 0
Hmm – no change – in fact the Log Space Used (%) has increased slightly from writing out the checkpoint log records (see How do checkpoints work and what gets logged). Committing the nested transaction did not allow the log to clear. And of course not, because a rollback can be issued at any time which will roll back all the way to the start of the outer transaction – so all log records are required until the outer transaction commits or rolls back.
And to prove it, I’ll commit the outer transaction and run a checkpoint:
COMMIT TRAN OuterTran; GO CHECKPOINT; GO DBCC SQLPERF ('LOGSPACE'); GO
Database Name Log Size (MB) Log Space Used (%) Status --------------------- ------------- ------------------ ----------- NestedXactsAreNotReal 12.05469 26.4339 0
And it drops right down.
Nested transactions: just say no! (a public service announcement from the nice folks at SQLskills.com :-)