Click here to Skip to main content
Click here to Skip to main content

Temporary Tables vs. Table Variables and Their Effect on SQL Server Performance

By , 28 May 2007
 

Introduction

There are three major theoretical differences between temporary tables:

CREATE table #T (…)
AND table-variables
DECLARE @T table (…)

Let's Begin

The first one is that transaction logs are not recorded for the table-variables. Hence, they are out of scope of the transaction mechanism, as is clearly visible from this example:

CREATE table #T (s varchar(128))
DECLARE @T table (s varchar(128))
INSERT into #T select 'old value #'
INSERT into @T select 'old value @'
BEGIN transaction
  UPDATE #T set s='new value #'
  UPDATE @T set s='new value @'
ROLLBACK transaction
SELECT * from #T
SELECT * from @T
s             
---------------
old value #

s                
---------------
new value @

After declaring our temporary table #T and our table-variable @T, we assign each one with the same "old value" string. Then, we begin a transaction that updates their contents. At this point, both will now contain the same "new value" string. But when we rollback the transaction, as you can see, the table-variable @T retained its value instead of reverting back to the "old value" string. This happened because, even though the table-variable was updated within the transaction, it is not a part of the transaction itself.

The second major difference is that any procedure with a temporary table cannot be pre-compiled, while an execution plan of procedures with table-variables can be statically compiled in advance. Pre-compiling a script gives a major advantage to its speed of execution. This advantage can be dramatic for long procedures, where recompilation can be too pricy.

Finally, table-variables exist only in the same scope as variables. Contrary to the temporary tables, they are not visible in inner stored procedures and in exec(string) statements. Also, they cannot be used in INSERT/EXEC statements.

But let's compare both in terms of performance.

At first, we prepare a test table with 1 million records:

CREATE table NUM (n int primary key, s varchar(128))
GO
SET nocount on
DECLARE @n int
SET @n=1000000
WHILE @n>0 begin
  INSERT into NUM
    SELECT @n,'Value: '+convert(varchar,@n)
  SET @n=@n-1
  END
GO

Now we prepare our test procedure T1:

CREATE procedure T1
  @total int
AS
  CREATE table #T (n int, s varchar(128))
  INSERT into #T select n,s from NUM
    WHERE n%100>0 and n<=@total
  DECLARE @res varchar(128)
  SELECT @res=max(s) from NUM
    WHERE n<=@total and
      NOT exists(select * from #T
      WHERE #T.n=NUM.n)
GO

Called with a parameter, which we will vary from 10, 100, 1000, 10'000, 100'000 up to 1'000'000, it copies the given number of records into a temporary table (with some exceptions, it skips records where n is divisible by 100), and then finds a max(s) of such missing records. Of course, the more records we give, the longer the execution is.

To measure the execution time precisely, I use the code:

DECLARE @t1 datetime, @n int
SET @t1=getdate()
SET @n=100 – (**)
WHILE @n>0 begin
  EXEC T1 1000 – (*)
  SET @n=@n-1 end
SELECT datediff(ms,@t1,getdate())
GO

(*) is a parameter to our procedure, it is varied from 10 to 1'000'000 (**) if an execution time is too short, I repeat the same loop 10 or 100 times. I run the code several times to get a result of a 'warm' execution.

The results can be found in Table 1 (see below).

Now let's try to improve our stored procedure by adding a primary key to the temporary table:

CREATE procedure T2
  @total int
AS
  CREATE table #T (n int primary key, s varchar(128))
  INSERT into #T select n,s from NUM
    WHERE n%100>0 and n<=@total
  DECLARE @res varchar(128)
  SELECT @res=max(s) from NUM
    WHERE n<=@total and
      NOT exists(select * from #T
      WHERE #T.n=NUM.n)
GO

Then, let's create a third one. With a clustered index, it works much better. But let's create the index AFTER we insert data into the temporary table – usually, it is better:

CREATE procedure T3
  @total int
AS
  CREATE table #T (n int, s varchar(128))
  INSERT into #T select n,s from NUM
    WHERE n%100>0 and n<=@total
  CREATE clustered index Tind on #T (n)
  DECLARE @res varchar(128)
  SELECT @res=max(s) from NUM
    WHERE n<=@total and
      NOT exists(select * from #T
      WHERE #T.n=NUM.n)
GO

Surprise! It not only takes longer for the big amounts of data; merely adding 10 records take an additional 13 milliseconds. The problem is that 'create index' statements force SQL Server to recompile stored procedures, and slows down the execution significantly.

Now let's try the same using table-variables:

CREARE procedure V1
  @total int
AS
  DECLARE @V table (n int, s varchar(128))
  INSERT into @V select n,s from NUM
    WHERE n%100>0 and n<=@total
  DECLARE @res varchar(128)
  SELECT @res=max(s) from NUM
    WHERE n<=@total and
      NOT exists(select * from @V V
      WHERE V.n=NUM.n)
GO

To our surprise, this version is not significantly faster than the version with the temporary table. This is a result of a special optimization SQL Server has for the create table #T statements in the very beginning of a stored procedure. For the whole range of values, V1 works better or the same as T1.

Now let's try the same with a primary key:

CREATE procedure V2
  @total int
AS
  DECLARE @V table (n int primary key, s varchar(128))
  INSERT into @V select n,s from NUM
    WHERE n%100>0 and n<=@total
  DECLARE @res varchar(128)
  SELECT @res=max(s) from NUM
    WHERE n<=@total and
      NOT exists(select * from @V V
      WHEREre V.n=NUM.n)
GO

The result is much better, but T2 outruns this version.

Table 1, using SQL Server 2000, time in ms

Records
T1 T2 T3 V1 V2
10 0.7 1 13.5 0.6 0.8
100 1.2 1.7 14.2 1.2 1.3
1000 7.1 5.5 27 7 5.3
10000 72 57 82 71 48
100000 883 480 580 840 510
1000000 45056 6090 15220 20240 12010

But the real shock is when you try the same on SQL Server 2005:

Table 2

N
T1 T2 T3 V1 V2
10 0.5 0.5 5.3 0.2 0.2
100 2 1.2 6.4 61.8 2.5
1000 9.3 8.5 13.5 168 140
10000 67.4 79.2 71.3 17133 13910
100000 700 794 659

Too long!

Too long!

1000000 10556 8673 6440

Too long!

Too long!

In some cases, SQL 2005 was much faster then SQL 2000 (marked with green). But in many cases, especially with huge amounts of data, procedures that used table variables took much longer (highlighted with red). In four cases, I even gave up waiting.

Conclusion

  1. There is no universal rule of when and where to use temporary tables or table variables. Try them both and experiment.
  2. In your tests, verify both sides of the spectrum – small amount/number of records and the huge data sets.
  3. Be careful with migrating to SQL 2005 when you use complicated logic in your stored procedures. The same code can run 10-100 times slower on SQL Server 2005!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Dmitry Tsuranoff
Russian Federation Russian Federation
Member
Born in 1968, Dmitry Tsuranoff is SQL professional who addresses issues and problems from the perspective of both a database developer and a DBA. He has worked in the United States, France and Russia. Currently, he is employed as Systems Architect and Team Manager at Lakeside Technologies, developer of high-performance Lakeside SQL Server Tools.
 
For more information, please visit http://www.lakesidesql.com.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionOperations on table variables ARE loggedmemberMartinSmith100000016 Feb '13 - 3:12 
For a review of the differences see What's the difference between a temp table and table variable in SQL Server?[^]
 

The reason for the better performance in the case of the small table variables is largely that cached table variables have slightly less maintenance overhead than cached temporary tables.
 
See http://dba.stackexchange.com/a/13412/3690[^]
GeneralMy vote of 5memberAnish Jain29 Nov '12 - 8:05 
good, I get everything at one place
GeneralSome notes on table variables and temp tablesmemberncarey6 Jun '07 - 10:51 
First, WRT to keys and indices: the execution plan for a stored procedure will not make use of any indices on a temporary table or table variable. This is documented in Books Online.
 
The only way to get anything other than a straight table scan on a temporary table or table variable within a stored procedure is to create a clustered primary key or clustered unique constraint: SQL Server then has no choice to use it as the data pages for the table become for all intents and purposes the leaf nodes of the b-tree structure for the index. There's no point to creating non-clustered indices as SQL Server won't actually use them. And you should, at least during development, create a primary key constraint and any relevant alternate key (unique index) constraints on temporary table/table variable. Not so much for run-time efficiency as for the enforcement of cardinality rules. You're much more likely to catch logical errors early on. As a hint, if you can't specify the cardinality of the result set for a select operation, you're likely asking the wrong question or don't understand the question you're asking.
 
And almost always, any query that's been shoehorned into uniqueness via SELECT DISTINCT is a big red flag indicating that most likely (A) somebody didn't know what they were doing or (B)the E-R/data model is incorrect. Something almost certainly (there are exceptions) needs to be fixed.
 
Second, performance for table variables is virtually guaranteed to go in the toilet once the size of the table variable gets "large" (c. >1000+ rows). My experiences suggest that the performance degradation isn't related to how much memory that table var occupies; the controlling factor is number of rows.
 
I'm not sure what the root cause is, but I always suspected that SQL Server is thrashing virtual memory (paging set), or they developers implemented some sort of paging algorithm for table variables that isn't very well thought out. We discovered this with stored procedures that were written with table variable (the SQL Server docs say they're better than temp tables all the way around). Worked find on the development and test boxen. Once it moved to production, the system screeched to a halt. Lots and lots of blocking. Bad. Couldn't figure out what the problem was -- execution plan looked great, everything's pretty much clustered index seeks or index seeks, etc. As an experiment, toggled everything from table variables to temporary tables -- a grown-up editor like emacs is a big help.
 
Voila! Blocking gone. Performance back to what one would expect.
 
The difference?
 
The dev/test databases had 10s of thousands of rows of data: production had 10s of millions.
 
Recommended Best Practice for Table Variables: Use temporary tables in preference to table variables and do not use table variables unless you in advance the upper bound of row count for the table variable.
 
Further -- it's a lot easier to debug/develop a stored procedure using temporary tables than it is using table variables. Executing the code for the stored procedure chunk-by-chunk, the temporary tables hang around in query analyzer until you drop them; table variables go out of scope as soon as the execution is complete.
 
Another Recommended Best Practice: Move the declaration of any table variables and creation of temporary tables to the very beginning of the stored procedure, before anything else is done.
 
Why? Every time a temporary table is created or a table variable is declared, the [current] execution plan for the stored procedure is invalidated and the stored procedure must be recompiled. Needless to say, compilation is (A) expensive and (B) can cause blocking as compilation (schema) locks are taken out on the involved objects and held for the duration of the [re-]compilation. Further, the stored procedure itself is locked and unavailable until the [re-]compilation is complete: anybody trying to execute it during compilation is blocked.
GeneralRe: Some notes on table variables and temp tablesmemberDmitry Tsuranoff6 Jun '07 - 21:50 
Thank you for your reply, but I dont agree with you Smile | :)
 
At first,
 
>the execution plan for a stored procedure will not make use of any indices on a temporary table or table variable. This is documented in Books Online.
 
Apparently, it is not true, even it is written in the Books Online.
On SQL 2005 SP2, the procedure V2 from my example generates the following execution plan (I show a part of it):
 
|--Stream Aggregate(DEFINE: ([Expr1006]=MAX([dbo].[NUM].[s])))
..|--Nested Loops(Left Anti Semi Join, WHERE: (@V.[n] as [V].[n]=[dbo].[NUM].[n]))
....|--Clustered Index Seek(OBJECT: ([dbo].[NUM].[PK__NUM__1B0907CE]), SEEK: ([dbo].[NUM].[n] <= [@total]) ORDERED FORWARD)
....|--Clustered Index Seek(OBJECT: (@V AS [V]), SEEK: ([V].[n] <= [@total]) ORDERED FORWARD)

 
Seek, not scan: Clustered Index Seek(OBJECT: (@V AS [V])
I found this improvement in SP2, but in some cases they did it before - even in MS SQL 2000.
 
The second: even when SQL server can not benefit from indices directly on the temporary tables, by adding a primary key you add a clustered index, and data become stored differently: you get a clustered-organized table instead of a heap-organized (and Microsoft claims Clustered-organized tables are much more efficient in all operations)
 
Finally, you write:
>Every time a temporary table is created or a table variable is declared, the [current] execution plan for the stored procedure is invalidated
 
It is true ONLY fro the temporary tables. When you have only table variables in you procedures, the plan normally is not recompiled and is known statically before you run it: for such procedures, you can get a 'Display estimated execution plan', while for procedures with temporary tables inside you can get an error (but SQL 2005 SP2 it can display execution plan in such cases, at least sometimes. Try on SQL 2000 and you see the difference).
 
P.S.
there is a new information which might be interesting
I performed the original experiment using SQL 2005 without any service packs, so I decided to do it again using SQL 2005 with SP2:
 
Microsoft SQL Server 2005 - 9.00.3042.00 (Build 2600: Service Pack 2)
 
i did not find any difference for the columns T1-T3 and V1, however, I see the dramatic improvement for the V2!
 
Here is a new result for the column V2 (compare with the old value without SP2)
 
N - V2 with SP2 (old value without SP2)
10 - 0.23 (0.2)
100 - 0.84 (2.5)
1000 - 7.1 (140)
10K - 69. (13910)
100K - 866 (too long)
1M - 19456 (too long)
 

GeneralRe: Some notes on table variables and temp tablesmemberPaladin_428 Jun '07 - 3:36 
One "trick" we've employed in the past is a "permanent temporary table". We insert the records we need into a normal table and then delete them when we are done. I'd be interested in a performance comparison of this approach versus the temp tables and table variables.
GeneralAmenmembermike good6 Jun '07 - 7:12 
For SQL 2000, if you compare query plans, you'll notice that when a table variable is involved the optimizer will not use parallelism (visually indicated in a query plan by a little yellow circle with black arrows).
 
I verified this to be true years ago on SQL 2000, but so far today have been unable to get anything to cause parallelism to come into play on my SQL 2005 dev box. Your timing results indicate that this has probably not changed in 2005.
 
Parallelism makes a big difference when dealing with big tables, and I bet this is the primary basis for your findings. Otherwise table vars would seem to be completely advantageous.
 
So table var are fine for very small work. But if they're going to be used to gather results from or join to very large tables, #temp tables will dramatically outperform table vars.
 
Mike Good
GeneralHmmmm...memberreshi9995 Jun '07 - 22:20 
I prefer not to use temp tables as I have found the multi user access degrades with all the access to the master database - Not that your results are wrong but I find table variables are a lot more reliable for certain operations, particularly for loops.
 
As with everything SQL though, I guess there is no consistent 'right' way Smile | :)
GeneralKeep table vars smallmemberSimulationofSai4 Jun '07 - 23:30 
Hmmm, I faced this situation while developing our BL in SQL Server and ran a lot of similar simulations. Table vars are good only as long as they can be maintained in memory. It can be a real boon in quite some cases. But it's severely limited by the actual memory available to SQL Server. If the number of rows grow too big, it starts getting paged, which is when one usually sees the performance graph going south.
In the end, we actually settled for Temp tables as the actual workload of the server does not affect it's optimizations too much and it can be scaled up to a fairly large size without fretting too about performance.
 
Thanks for the article.
 
SG

QuestionThanks - but which service packs?memberPete Appleton4 Jun '07 - 22:54 
Thanks for the very informative & useful article - got my 5. Just one question; which service packs were applied to the DB's used for the comparison? One of the major fixes for 2K5 was meant to be performance, so I'm interested to know if the service pack was applied.
 
--
What's a signature?

AnswerRe: Thanks - but which service packs?memberDmitry Tsuranoff5 Jun '07 - 1:03 
Thank you for the good question.
 
I performed the original experiment using SQL 2005 without any service packs, so I decided to do it again using SQL 2005 with SP2:
 
Microsoft SQL Server 2005 - 9.00.3042.00 (Build 2600: Service Pack 2)
 
i did not find any difference for the columns T1-T3 and V1, however, I see the dramatic improvement for the V2!
 
Here is a new result for the column V2 (compare with the old value without SP2)
 
N - V2 with SP2 (old value without SP2)
10 - 0.23 (0.2)
100 - 0.84 (2.5)
1000 - 7.1 (140)
10K - 69. (13910)
100K - 866 (too long)
1M - 19456 (too long)
 
So you're right, they worked hard on the performance.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 28 May 2007
Article Copyright 2007 by Dmitry Tsuranoff
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid