Table Variables and Transactions in SQL Server: SQL in Sixty Seconds #007

Table variables and transactions behave differently from temporary tables, because a ROLLBACK does not remove rows from a table variable.

Gouache painting of two shallow trays, one held by a red ribbon and the other containing loosely arranged wooden pieces.

This episode grew out of my TechEd India 2012 session on misconceptions and resolutions. A developer who expects a rollback to clear everything reads the result as a bug. It is the documented behavior. I tested each example below on SQL Server 2025, and the counts I quote are the ones it returned.

Watch the Original Episode

A Full Rollback

This example inserts the same two rows into a temporary table and a table variable, then rolls back. Run it in a new query window with no open transaction, because a ROLLBACK ends the outer one.

CREATE TABLE #Scores (TeamId int, Points int);
DECLARE @Scores TABLE (TeamId int, Points int);
BEGIN TRANSACTION;
INSERT INTO #Scores VALUES (1, 10), (2, 20);
INSERT INTO @Scores VALUES (1, 10), (2, 20);
ROLLBACK TRANSACTION;
SELECT 'Temporary table' AS Storage, COUNT(*) AS RowsLeft FROM #Scores
UNION ALL
SELECT 'Table variable', COUNT(*) FROM @Scores;
DROP TABLE #Scores;

The temporary table had 0 rows afterwards. The table variable had 2. Both inserts succeeded. The rollback undid only the work that belongs to the transaction. A table variable does not belong to it, so its rows stayed.

A Savepoint Rollback

A partial rollback shows the same split. The table variable keeps rows from after the savepoint, and the temporary table loses them.

DECLARE @Steps TABLE (StepNo int, Note varchar(20));
CREATE TABLE #Steps (StepNo int, Note varchar(20));
BEGIN TRANSACTION;
INSERT INTO @Steps VALUES (1, 'before savepoint');
INSERT INTO #Steps VALUES (1, 'before savepoint');
SAVE TRANSACTION AfterStep1;
INSERT INTO @Steps VALUES (2, 'after savepoint');
INSERT INTO #Steps VALUES (2, 'after savepoint');
ROLLBACK TRANSACTION AfterStep1;
COMMIT TRANSACTION;
SELECT 'Table variable' AS Storage, COUNT(*) AS RowsLeft, MAX(StepNo) AS LastStep FROM @Steps
UNION ALL
SELECT 'Temporary table', COUNT(*), MAX(StepNo) FROM #Steps;
DROP TABLE #Steps;

The table variable kept both rows and its last step was 2. The temporary table kept one row, and its last step was 1.

Quick reference card: Table Variables and Transactions in SQL Server: SQL in Sixty Seconds #007

A Failed Statement Still Inserts Nothing

This does not mean a table variable ignores every error. One statement is all or nothing, whatever it writes to. I inserted three rows in one statement, and the last row divided by zero.

DECLARE @Batch TABLE (Id int);
BEGIN TRY
    INSERT INTO @Batch (Id) SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 1 / 0;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS ErrorNumber, (SELECT COUNT(*) FROM @Batch) AS RowsInTableVariable;
END CATCH;

The error was 8134 and the table variable held 0 rows. Only a rollback of a whole transaction skips it.

Use the Survivor for Logging

The surviving rows are useful. A CATCH block can write a message into a table variable and roll back. It then saves the message to a permanent table. A permanent table written before the rollback would lose its rows. Here are two small tables for the example.

CREATE TABLE dbo.Orders (OrderId int PRIMARY KEY, Amount decimal(8,2) NOT NULL);
CREATE TABLE dbo.ImportLog (LogId int IDENTITY(1,1) PRIMARY KEY, Message nvarchar(200) NOT NULL);
DECLARE @Log TABLE (Message nvarchar(200));
BEGIN TRY
    BEGIN TRANSACTION;
    INSERT INTO dbo.Orders VALUES (1, 25.00);
    INSERT INTO @Log VALUES (N'Order 1 inserted');
    INSERT INTO dbo.Orders VALUES (1, 99.00);
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    INSERT INTO @Log VALUES (CONCAT(N'Error ', ERROR_NUMBER(), N': ', ERROR_MESSAGE()));
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
    INSERT INTO dbo.ImportLog (Message) SELECT Message FROM @Log;
END CATCH;
SELECT (SELECT COUNT(*) FROM dbo.Orders) AS OrderRows, Message FROM dbo.ImportLog ORDER BY LogId;

The duplicate key raised error 2627. Afterwards dbo.Orders held 0 rows, and the log held two messages: the insert and the error with its duplicate value. Neither message was lost with the rollback.

Clear a Table Variable Before a Retry

The same behavior bites a retry loop. A failed attempt rolls back, and the loop tries again. The table variable still holds the rows from that failed attempt. Two attempts left four rows in my test, with every order twice. Clearing the table at the start of each attempt left two.

DECLARE @Staging TABLE (OrderId int);
DECLARE @Attempt int = 1;
WHILE @Attempt <= 2
BEGIN
    BEGIN TRANSACTION;
    DELETE FROM @Staging;
    INSERT INTO @Staging VALUES (101), (102);
    ROLLBACK TRANSACTION;
    SET @Attempt += 1;
END;
SELECT COUNT(*) AS RowsInStaging FROM @Staging;

The ROLLBACK stands in for a failed attempt here. In real code the retry would follow an error such as a deadlock. Without the DELETE, the result was 4.

Create Temporary Tables Before the Transaction

A temporary table created inside a transaction is rolled back with it. After my ROLLBACK, a query on the table failed with Msg 208, invalid object name. Create it before BEGIN TRANSACTION when you plan to inspect it afterwards.

Check the Transaction Count

After my rollback, @@TRANCOUNT was 0 and the table variable still held its row. When a result looks wrong, read @@TRANCOUNT first. A ROLLBACK in a nested call ends every open transaction. The table variable keeps whatever it received before that point.

Keep the Conclusion Narrow

This result does not make a table variable permanent storage. Its scope is the batch or procedure that declares it, and it vanishes when that ends. It says nothing about speed or about where the rows are stored. Those are separate questions. Choose the structure for the behavior you need.

Gouache painting of a cheerful database wearing headphones beside a stopwatch

Related Transaction Examples

A table variable is not a durable rollback log, it is temporary state with its own behavior.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL in Sixty Seconds, SQL Transactions, SQL Variable, Temp Table
Previous Post
SQL Server Indexing Basics: SQL in Sixty Seconds #006
Next Post
MAXDOP 1 in SQL Server: SQL in Sixty Seconds #008

Related Posts

4 Comments. Leave new

  • Shantanu Gupta
    March 21, 2012 10:17 am

    @Pinal: Thank you so much for putting some light on this. I noticed this behaviour just 2 week ago and trying to figure out why this is the case inspite of both the tables getting created in tempdb.tables
    I also observed that any local variable declared on the same scenario works like local table variable. Please let me know WHY this is the case. Is it the case that Transactions are associated with connection and scope of Temp table is wrt connection while table variable is local to the scope ?

    Reply
  • Thank you. I’ve learnt this from you!

    Reply
  • gr8! reference

    Reply
  • Hey Pinal,
    I would require your inputs on one of the issue which I encounter’s at random interval.

    Issue Is with the SP logic. There are 2 Tables one is Master Table ‘Day’ which has day wise entry and other is the transaction table ‘Date’ which is date wise. The SP logic is implemented using Table Variable and CTE.

    SP Logic is as follows:

    1) I am checking, if the date has an entry in the transaction table for that date. If not find any entry for that date then I am removing the day of that date by Date function and inserting the data of that day which is taken from DAY(Master) table and inserting into Table Variable
    2) After inserting into table variable, Further I insert into Transaction table row by row.
    3)After the insertion process , I have used CTE for displaying the result.

    Now the challenge is like this… At the time time of insertion from master to table variable or table variable to transaction table, A column of Nvarchar Datatype is getting null,however master table ‘DAY’ has the entry for that column. This issue happens randomly and I am unable to trace it where it is causing the issue.

    If you can help me with this at the earliest that will be more helpful for me…

    Thanks in advance..:-)

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.