![]() |
|
|
|
By: Ben Snaidero | Read Comments (15) | Related Tips: More > T-SQL |
If you've been a DBA for any amount of time I'm sure you've been asked the question: Which is better to use, a temp table or a table variable? There are technical reasons why to use one over the other, but being that I am usually just interested in performance I am more concerned with which one will be faster and use fewer resources. In this tip we will compare the performance of these two temporary objects using a few straightforward scenarios.
For those of us that aren't familiar with all the technical differences between the two objects here is a great tip that covers all you need to know regarding the differences between temporary tables and table variables.
For this test scenario we are going to load data into four tables, two will be temporary tables and two will be table variables. Each of these object groups will have one small table with only 2000 records and one larger one with 1000000 records so we can see if there are any differences based on the size of the temporary object as well.
-- Table creation logic CREATE TABLE #temptable ([col1] [int] NOT NULL primary key,[col2] [int] NULL,
[col3] [int] NULL,[col4] [varchar](50) NULL)
DECLARE @tablevariable TABLE ([col1] [int] NOT NULL primary key,[col2] [int] NULL,
[col3] [int] NULL,[col4] [varchar](50) NULL)
CREATE TABLE #bigtemptable ([col1] [int] NOT NULL primary key,[col2] [int] NULL,
[col3] [int] NULL,[col4] [varchar](50) NULL)
DECLARE @bigtablevariable TABLE ([col1] [int] NOT NULL primary key,[col2] [int] NULL,
[col3] [int] NULL,[col4] [varchar](50) NULL)
-- Create index logic CREATE NONCLUSTERED INDEX [IX_temptable] ON #temptable ([col2] ASC) CREATE NONCLUSTERED INDEX [IX_bigtemptable] ON #bigtemptable ([col2] ASC) -- Populate tables DECLARE @val INT SELECT @val=1 WHILE @val <= 2000 BEGIN INSERT INTO #temptable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') INSERT INTO @tablevariable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') INSERT INTO #bigtemptable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') INSERT INTO @bigtablevariable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') SELECT @val=@val+1 END WHILE @val <= 1000000 BEGIN INSERT INTO #bigtemptable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') INSERT INTO @bigtablevariable (col1, col2, col3, col4) VALUES (@val,@val,@val,'TEST') SELECT @val=@val+1 END
To test the performance of these objects we are going to run statements for each of the 4 basic DML operations, SELECT/INSERT/UPDATE/DELETE. For each operation we are going run statements that effect both single and multiple rows. We will also break up the statements so that each operation will test referencing primary key columns as well as non-key columns that are both indexed and not indexed. This will allow us to see how much benefit if any we get from having indexes on a column. Only temporary tables can have additional non-clustered indexes defined. Table variables can only have primary keys defined at creation so this may be an important factor to consider when it comes to performance. The comments inline within the script define what the statements are testing. Here is the code for all of our test scenarios.
-- simple insert INSERT INTO #temptable (col1, col2, col3, col4) VALUES (2005,2005,2005,'TEST') INSERT INTO @tablevariable (col1, col2, col3, col4) VALUES (2005,2005,2005,'TEST') INSERT INTO #bigtemptable (col1, col2, col3, col4) VALUES (1000005,1000005,1000005,'TEST') INSERT INTO @bigtablevariable (col1, col2, col3, col4) VALUES (1000005,1000005,1000005,'TEST')
-- select single row where primary key column equals value SELECT * FROM #temptable where col1=1000 SELECT * FROM @tablevariable where col1=1000 SELECT * FROM #bigtemptable where col1=600000 SELECT * FROM @bigtablevariable where col1=600000
-- select range of rows where primary key column between values SELECT * FROM #temptable where col1 between 450 and 500 SELECT * FROM @tablevariable where col1 between 450 and 500 SELECT * FROM #bigtemptable where col1 between 100000 and 100500 SELECT * FROM @bigtablevariable where col1 between 100000 and 100500
-- select single row where other indexed column equals value SELECT * FROM #temptable where col2=1050 SELECT * FROM @tablevariable where col2=1050 SELECT * FROM #bigtemptable where col2=650000 SELECT * FROM @bigtablevariable where col2=650000
-- select range of rows where other indexed column between values SELECT * FROM #temptable where col2 between 500 and 550 SELECT * FROM @tablevariable where col2 between 500 and 550 SELECT * FROM #bigtemptable where col2 between 150000 and 150500 SELECT * FROM @bigtablevariable where col2 between 150000 and 150500
-- select single row where other nonindexed column equals value SELECT * FROM #temptable where col3=1100 SELECT * FROM @tablevariable where col3=1100 SELECT * FROM #bigtemptable where col3=700000 SELECT * FROM @bigtablevariable where col3=700000
-- select range of rows where other nonindexed column between values SELECT * FROM #temptable where col3 between 550 and 600 SELECT * FROM @tablevariable where col3 between 550 and 600 SELECT * FROM #bigtemptable where col3 between 200000 and 200500 SELECT * FROM @bigtablevariable where col3 between 200000 and 200500
-- update single row where primary key column equals value UPDATE #temptable set col4='TESTUPDATE' where col1=1150 UPDATE @tablevariable set col4='TESTUPDATE' where col1=1150 UPDATE #bigtemptable set col4='TESTUPDATE' where col1=750000 UPDATE @bigtablevariable set col4='TESTUPDATE' where col1=750000
-- update range of rows where primary key column between values UPDATE #temptable set col4='TESTUPDATE' where col1 between 600 and 650 UPDATE @tablevariable set col4='TESTUPDATE' where col1 between 600 and 650 UPDATE #bigtemptable set col4='TESTUPDATE' where col1 between 250000 and 250500 UPDATE @bigtablevariable set col4='TESTUPDATE' where col1 between 250000 and 250500
-- update single row where other indexed column equals value UPDATE #temptable set col4='TESTUPDATE' where col2=1200 UPDATE @tablevariable set col4='TESTUPDATE' where col2=1200 UPDATE #bigtemptable set col4='TESTUPDATE' where col2=800000 UPDATE @bigtablevariable set col4='TESTUPDATE' where col2=800000
-- update range of rows where other indexed column between values UPDATE #temptable set col4='TESTUPDATE' where col2 between 650 and 700 UPDATE @tablevariable set col4='TESTUPDATE' where col2 between 650 and 700 UPDATE #bigtemptable set col4='TESTUPDATE' where col2 between 300000 and 300500 UPDATE @bigtablevariable set col4='TESTUPDATE' where col2 between 300000 and 300500
-- update single row where other nonindexed column equals value UPDATE #temptable set col4='TESTUPDATE' where col3=1250 UPDATE @tablevariable set col4='TESTUPDATE' where col3=1250 UPDATE #bigtemptable set col4='TESTUPDATE' where col3=850000 UPDATE @bigtablevariable set col4='TESTUPDATE' where col3=850000
-- update range of rows where other nonindexed column between values UPDATE #temptable set col4='TESTUPDATE' where col3 between 700 and 750 UPDATE @tablevariable set col4='TESTUPDATE' where col3 between 700 and 750 UPDATE #bigtemptable set col4='TESTUPDATE' where col3 between 350000 and 350500 UPDATE @bigtablevariable set col4='TESTUPDATE' where col3 between 350000 and 350500
-- delete single row where primary key column equals value DELETE FROM #temptable where col1=1300 DELETE FROM @tablevariable where col1=1300 DELETE FROM #bigtemptable where col1=900000 DELETE FROM @bigtablevariable where col1=900000
-- delete range rows where primary key column between values DELETE FROM #temptable where col1 between 750 and 800 DELETE FROM @tablevariable where col1 between 750 and 800 DELETE FROM #bigtemptable where col1 between 400000 and 400500 DELETE FROM @bigtablevariable where col1 between 400000 and 400500
-- delete single row where other indexed column equals value DELETE FROM #temptable where col2=1350 DELETE FROM @tablevariable where col2=1350 DELETE FROM #bigtemptable where col2=950000 DELETE FROM @bigtablevariable where col2=950000
-- delete range of rows where other indexed column between values DELETE FROM #temptable where col2 between 800 and 850 DELETE FROM @tablevariable where col2 between 800 and 850 DELETE FROM #bigtemptable where col2 between 450000 and 450500 DELETE FROM @bigtablevariable where col2 between 450000 and 450500
-- delete single row where other nonindexed column equals value DELETE FROM #temptable where col3=1400 DELETE FROM @tablevariable where col3=1400 DELETE FROM #bigtemptable where col3=1000000 DELETE FROM @bigtablevariable where col3=1000000
-- delete range of rows where other nonindexed column values DELETE FROM #temptable where col3 between 900 and 950 DELETE FROM @tablevariable where col3 between 900 and 950 DELETE FROM #bigtemptable where col3 between 500000 and 500500 DELETE FROM @bigtablevariable where col3 between 500000 and 500500
Note: This script must be run in a single batch (table setup and test scenario) as the scope of the table variables are limited to the current batch of sql statements.

The SQL Profiler trace from the INSERT statements show that the table variable outperforms the temporary table whether dealing with a small or large temporary object. This can probably be attributed to the fact that the temporary table has an extra index on the table that needs to be updated as well.

The SQL Profiler trace from the SELECT statements tell a different story. When querying rows based on the primary key column or the non-indexed column we get the same performance from both objects. However, when we query rows using the indexed column of the temporary table, which is not indexed in the table variable since this is not available for table variables, we see a really big increase in performance across all measures for the temporary table. In fact, using this index to query a specific value from the temporary table yields almost the same performance regardless of the size of the table.

The SQL Profiler trace from the UPDATE statements shows a similar trend to that of the SELECT statements. When updating rows based on either the primary key or non-indexed column the performance is similar. When updating rows based on the indexed column the temporary table performs quite a bit better.

With the SQL Profiler trace for the DELETE statements we see an interesting result. When deleting rows based on the primary key the table variable outperforms the temporary table. As with the SQL Profiler results of the INSERT statement this is probably due to the fact that an extra index has to be updated. As with the other scenarios when deleting based on the indexed column the temporary table performs better than the table variable. When deleting rows based on the column with no index we see similar performance between the two objects with the temporary table only slightly better when dealing with a range of records. One thing I want to add with regards DELETEs is that I would not be as concerned with its performance since in most cases if you don't need the data in your temporary object then you can exclude it when performing the SELECT/INSERT rather than removing it after the fact.
As we can see from the results above a temporary table generally provides better performance than a table variable. The only time this is not the case is when doing an INSERT and a few types of DELETE conditions. This increase in performance is especially evident when dealing with larger data sets as the ability to create indexes on the temporary table speeds up query execution significantly. When the data set is small the difference in performance is almost negligible. In my opinion if you are doing a lot of data manipulation with the temporary object after it has been created and the data set is fairly large a temporary table is the best option. As always it's best to test yourself to confirm which object gives you the best performance for your particular application.
| Tuesday, December 04, 2012 - 8:53:21 AM - Dan | Read The Tip |
|
Nice to see someone else confirm my experience - e.g. I'm not crazy! From my testing, temp tables are as good as or outperform table variables. The clear advantage to me is the ability to pass a table variable as a parameter, but the use case for those seems limited, and the overhead (creating a table variable type) is too much. Easier to just use temp tables, even then. I have heard that the optimizer / sql engine will convert large table variables to temporary tables anyway, under the covers. Can anyone confirm? |
|
| Tuesday, December 04, 2012 - 8:53:44 AM - Jeff Moden | Read The Tip |
|
Nice article. Good to see someone quantify these differences with code. In many (most?) cases, the temporary structure is made to contain only the data that is actually needed. The reason why that is important is because that usually means that full table scans will be made anyway which usually negates the need for any indexing. There are always exceptions, of course. Whether there are indexes or not, Temp Tables can frequently be batch loaded with minimal logging using SELECT INTO which can give Temp Tables a performance advantage over Table Variables. There is another "performance" advantage that Temp Tables can have over Table Variables and you eluded to it. The contents of a Temp Table will persist in SSMS allowing a programmer to incrimentally troubleshoot code without having to include all of the previous code that builds and populates a Table Variable. That sometimes makes both development and troubleshooting go quite a bit faster.
|
|
| Tuesday, December 04, 2012 - 8:57:32 AM - Jeff Moden | Read The Tip |
|
To answer the question about the conversion of large Table Variables to Temp Tables... Both Temp Tables and Table Variables build their structure in TempDB. As with any good database engine, those structures start out in memory and spill to disk if they get too big. You can typically see Table Variables in TempDB as tables that begin with a # sign and have an all numeric digit name. |
|
| Thursday, December 06, 2012 - 5:34:25 AM - Madhivanan | Read The Tip |
|
In addition to what Jeff Moden said, you can see table variables in tempdb only from version 2008 onwards. Here is a post about it http://beyondrelational.com/modules/2/blogs/70/posts/13664/storage-of-table-variable.aspx |
|
| Thursday, December 06, 2012 - 7:10:04 AM - Martin Smith | Read The Tip |
|
@Madhivanan Not sure why you say just 2008. Even on 2000 you can see them in tempdb with
DECLARE @T TABLE (X INT)
SELECT * FROM sysobjects WHERE name LIKE '#%'
I've written a quite detailed piece about the differences between temporary tables and table variables here.
|
|
| Thursday, December 06, 2012 - 7:14:11 AM - Martin Smith | Read The Tip |
|
(Make sure you are in tempdb if running the code above!) |
|
| Thursday, December 06, 2012 - 11:13:40 AM - Try other cases as well. | Read The Tip |
|
This was a reasonable first test, and I'm glad to see actual numbers, but I'd have to suggest some other factors to look at, most of which are highly likely to favor #temp tables at many non-trivial data sizes. 1) For any unique column (even more with the unique, contiguous data you used) Table variable nonclustered unique indexes can be created using UNIQUE constraints (http://www.sqlservercentral.com/Forums/Topic535656-1338-1.aspx#bm535734) DECLARE @MyTabVar TABLE( PKCol intNOTNULLPRIMARYKEYCLUSTERED,UniqueCol intNOTNULLUNIQUE) |
|
| Sunday, December 09, 2012 - 10:49:04 AM - Eric Russell | Read The Tip |
|
One considering when persisting and modifying large temporary resultsets is transaction logging. I understand why temp tables are logged, because they are included in batch level transactions and so technically should be included in a rollback, although at least in my experience there rarely a practical use for that. However, with temp tables you have the option of creating the table using SELECT INTO, which will not not transaction log the inserts the same way it will if you first create the table and then insert it. Table variables are not even included in transactions, just like other variables they arn't recovered from a rollback, but for some reason they are still transaction logged like temp tables. That's unfortunate, and if it were not the case, table variables would be clearly favored in a wider array of scenarios.
|
|
| Sunday, December 09, 2012 - 3:45:40 PM - Martin Smith | Read The Tip |
|
@Eric Table variables are subject to logging because if in an error occurs during the execution of the statement it needs to be rolled back. For example DECLARE @T TABLE ( P INT PRIMARY KEY ) INSERT INTO @T VALUES (1),(2) INSERT INTO @T VALUES (3),(4),(1) SELECT * FROM @T
The statement encounters a PK uniqueness violation so the insert of values 3 & 4 is rolled back. In 2008+ "INSERT ... SELECT" can be minimally logged in the same way as "SELECT ... INTO". Temporary tables can still have advantages when it comes to logging though. e.g table variables do not support "TRUNCATE". One logging advantage of table variables is that the system transactions on them are independent of the surrounding user transaction so a long running transaction that writes to table variables will not prevent the tempdb transaction log from being truncated. |
|
| Sunday, December 09, 2012 - 11:46:36 PM - Madhivanan | Read The Tip |
|
@Martin Smith Do information_schema.tables show up the table variables in version 2000? I think I said that keeping information_schma.tables in mind |
|
| Monday, December 10, 2012 - 3:48:40 AM - Martin Smith | Read The Tip |
|
@Madhivanan - Yes in MSDE 2000, running the following
declare @t table(i int)
select * from tempdb.INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE '#%'
Returns
TABLE_CATALOG TABLE_SCHEMA TABLE_NAME TABLE_TYPE --------------- -------------- ------------- ---------- tempdb dbo #108B795B BASE TABLE |
|
| Tuesday, December 11, 2012 - 9:14:50 PM - TimothyAWiseman | Read The Tip |
|
Thanks for the excellent tip. This confirms what I have generally seen as well. Personally, I think table variables often result in slightly cleaner code and they can be passed as parameters when that matters. But temp tables will almost always perform at least as well and often better, and they give you more options for indexing. Generally, I use a table variable when I am dealing with very small datasets (less than 100 rows) and temp tables the rest of the time. |
|
| Wednesday, March 06, 2013 - 11:55:37 PM - asit kaushik | Read The Tip |
|
my 2 cents the basic difference when we ask anybody is that table variables are memory resident whereas temporary tables are temdb resident which is very untrue which is been proven in many blog posts and can be proved with the below code snippet use tempdb ---You will see we have a # table which is for table variable--- But to me the question arises what is the reason of keeping table variable if we have a similar temprary table. I could only see the case of functions otherwise i could not get any reason where we can justify the existance of table variable. |
|
| Tuesday, March 12, 2013 - 10:34:45 AM - AvidaEinav | Read The Tip |
|
Another huge deal is the statistics.. Table varible don't keep statistics, this causes very poor performance when doing joins ex.. Temp table do keeps statistic! |
|
| Tuesday, June 11, 2013 - 10:31:46 AM - Robert J. Good | Read The Tip |
|
Thanks for the article Ben. Not the results I would have expected. |
|
|
privacy | disclaimer | copyright | advertise | about authors | contribute | feedback | giveaways | user groups Some names and products listed are the registered trademarks of their respective owners. Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com |