3 Things Developers Should Know Before Using the SQL Server Table Variable

By Pinal Dave on July 30, 2015


There are many myths and incorrect information shared online about the SQL Server table variable, and it would be easy for a SQL Server developer to assume that, based on the misinformation, using a table variable instead of a temporary table was a good idea. Using table variable is in fact easy, but there are things a developer should know before making such a decision. In this article, we will debunk some of the myths about table variable using real-world examples.

First, let’s understand how to use a table variable. It is very simple, as shown below:

 

DECLARE @Employees TABLE

(

      EmployeeID INT,

      FirstName VARCHAR(30),

      LastName VARCHAR(30),

      Gender CHAR(1)

)

Then we can use this to insert data, select data and use them as tables.

Transaction scope and lifetime

Just like any local variable we create with a DECLARE statement, a table variable is also scoped to the stored procedure, batch, or user-defined function. On the other hand, temporary tables are only available to the current connection to the database for the current user. They are dropped when the connection is closed. Whenever there is a rollback of a transaction, it does not affect the table variables. As we can see in the output of the query below – even after a rollback, we are still seeing data in table variable whereas data in temporary table is cleaned up.

 

SET NOCOUNT ON

DECLARE @Employee TABLE(Id INT)

CREATE TABLE #Employee (Id INT)



BEGIN TRAN



INSERT #Employee

VALUES(1),(2)



INSERT @Employee

VALUES(1),(2)



/*Both have 2 rows*/

SELECT Id AS '#Table_ID' FROM #Employee

SELECT Id AS '@Table_ID' FROM @Employee



ROLLBACK



PRINT 'AFTER ROLLBACK'

PRINT '==============='

/*Only table variable now has rows*/

SELECT Id AS '#Table_ID' FROM #Employee

SELECT Id AS '@Table_ID' FROM @Employee

DROP TABLE #Employee

 

Here is the output:

tv-01

Table variables are accessible only within the batch or scope in which they are declared.

 

SET NOCOUNT ON
DECLARE @Employee TABLE(Id INT)
CREATE TABLE #Employee (Id INT)

INSERT #Employee
VALUES(1),(2)

INSERT @Employee
VALUES(1),(2)

/*Both have 2 rows*/
SELECT Id AS '#Table_ID' FROM #Employee
SELECT Id AS '@Table_ID' FROM @Employee
GO		

-- GO is a default batch separator in SSMS

SELECT Id AS '#Table_ID' FROM #Employee
SELECT Id AS '@Table_ID' FROM @Employee  -- this would fail as it’s in new batch

 

The example above would result in this error message:

 

Msg 1087, Level 15, State 2, Line 19

Must declare the table variable "@Employee"

 

 

Statistics and indexes

Table variables do not allow ALTER to the definition after they are declared. And prior to SQL Server 2014, they don’t allow non-clustered indexes to be created.

We also need to keep in mind that the estimation of the number of rows might be way off as compared to actual data in the table.

Here is the script which you can run to see the effect.

 

USE tempdb

GO

IF OBJECT_ID('PermanentEmployeeTable') IS NOT NULL

DROP TABLE PermanentEmployeeTable

GO

CREATE TABLE PermanentEmployeeTable (EmployeeID INT)

GO

CREATE INDEX idx_PermanentEmployeeTable ON PermanentEmployeeTable (EmployeeID)

GO

--insert 100000 rows into the PermanentEmployeeTable table

SET NOCOUNT ON

BEGIN TRANSACTION

DECLARE @loop INT

SET @loop = 0

WHILE @loop < 100000

BEGIN

INSERT INTO PermanentEmployeeTable

VALUES (@loop)

SET @loop = @loop + 1

END

COMMIT TRANSACTION

GO

--update stats with fullscan

UPDATE STATISTICS PermanentEmployeeTable WITH FULLSCAN



SET NOCOUNT ON

DECLARE @EmployeeTableVariable TABLE (EmployeeID INT)

BEGIN TRANSACTION

DECLARE @loop INT

SET @loop = 0

WHILE @loop < 100000

BEGIN

INSERT INTO @EmployeeTableVariable

VALUES (@loop)

SET @loop = @loop + 1

END

COMMIT TRANSACTION

SET STATISTICS PROFILE ON

SELECT *

FROM @EmployeeTableVariable tv

INNER JOIN PermanentEmployeeTable t ON tv.EmployeeID = t.EmployeeID

GO

SET STATISTICS PROFILE OFF

 

If we look at the query plan, we can see that the table variable has an estimation of only one row which is causing nested loop join for huge rows.

tv-02

 

The problem shown above can be solved using option recompile in last statement as shown below.

 

SELECT *

FROM @EmployeeTableVariable tv

INNER JOIN PermanentEmployeeTable t  ON tv.EmployeeID = t.EmployeeID  OPTION (RECOMPILE)

 

This would cause the query optimizer to recompile the statement during run time and then it can look into the rows in the table variable.

This is another myth about table variable: if table variables have smaller sets of data, then they would be only memory resident. This information is not correct, and here’s the quick proof.

 

DECLARE @TableVariable TABLE(Column1 INT)



INSERT INTO @TableVariable VALUES(1),(2)



SELECT sys.fn_PhysLocFormatter(%%physloc%%) AS [File:Page:Slot]

FROM @TableVariable

 

tv-03

We can see here the location of the table variable. Both rows are stored in a physical page 540 in fileID 1. Along with the storage, it is also important to note that they have entries in transaction log file of tempdb system database.

It’s generally said that the performance of stored procedures may improve by using table variables, but whether this is the case with you or not, can’t easily be forecasted.

 

Related Posts

Leave a Reply