SQL Server CTE vs Temp Table vs Table Variable Performance Test

Problem

In a previous article, SQL Server Temp Table vs Table Variable Performance Testing, we looked at SQL Server performance differences between using a temp table and a table variable for different DML operations. One of the comments suggested comparing these results to using a Common Table Expression (CTE) for similar operations. In this article we will go through a few simple SELECT queries in order to compare the performance in SQL Server using a temporary table, table variable and a CTE. We will compare the execution times of each query as well as the system resources needed to complete each operation.

Solution

While a CTE is a really good tool it does have some limitations as compared with a temporary table or a table variable. This biggest difference is that a CTE can only be used in the current query scope whereas a temporary table or table variable can exist for the entire duration of the session allowing you to perform many different DML operations against them. That said the CTE is still a really useful tool which can make your T-SQL code more readable as well as makes writing recursive queries much less complex as the CTE can reference itself.

Compare Temp Table, Table Variable and CTE

For this test we will go through an example using the Adventureworks2014 database and gather all the order header data for each customers most recent order.

Below is the T-SQL for each of our test query types.

-- CTE
WITH t (customerid, lastorderdate) AS 
 (SELECT customerid, max(orderdate) 
  FROM sales.SalesOrderHeader
  GROUP BY customerid)
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
GO
-- Temporary table
CREATE TABLE #temptable (customerid [int] NOT NULL PRIMARY KEY, lastorderdate [datetime] NULL);
INSERT INTO #temptable
SELECT customerid, max(orderdate) as lastorderdate 
FROM sales.SalesOrderHeader
GROUP BY customerid;
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN #temptable t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
DROP TABLE #temptable
GO
-- Table variable
DECLARE @tablevariable TABLE (customerid [int] NOT NULL PRIMARY KEY, lastorderdate [datetime] NULL);
INSERT INTO @tablevariable
SELECT customerid, max(orderdate) as lastorderdate 
FROM sales.SalesOrderHeader
GROUP BY customerid;
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN @tablevariable t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
GO

Looking at SQL Profiler results from these queries (each were run 10 times and averages are below) we can see that the CTE just slightly outperforms both the temporary table and table variable queries when it comes to overall duration. The CTE also uses less CPU than the other two options and performs fewer reads (significant fewer reads that the table variable query).

Query TypeReadsWritesCPUDuration (ms)
CTE1378047497
Temp table214651109544
Table variable13374851297578

Another Example Comparing Performance of Temp Table, Table Variable and CTE

Let’s also test getting the most recent order header data for just a single customer to see if there is any difference in performance for a smaller data set.

Here are the updated queries which add a WHERE clause to each statement we tested above.

-- CTE
WITH t (customerid, lastorderdate) AS 
 (SELECT customerid, max(orderdate) 
  FROM sales.SalesOrderHeader
  WHERE customerid=27604 
  GROUP BY customerid)
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
GO
--Temp table
CREATE TABLE #temptable (customerid [int] NOT NULL PRIMARY KEY, lastorderdate [datetime] NULL);
INSERT INTO #temptable
SELECT customerid, max(orderdate) as lastorderdate 
FROM sales.SalesOrderHeader
WHERE customerid=27604
GROUP BY customerid;
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN #temptable t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
DROP TABLE #temptable
GO
--Table variable
DECLARE @tablevariable TABLE (customerid [int] NOT NULL PRIMARY KEY, lastorderdate [datetime] NULL);
INSERT INTO @tablevariable
SELECT customerid, max(orderdate) as lastorderdate 
FROM sales.SalesOrderHeader
WHERE customerid=27604
GROUP BY customerid;
SELECT * 
FROM sales.salesorderheader soh
INNER JOIN @tablevariable t ON soh.customerid=t.customerid AND soh.orderdate=t.lastorderdate
GO

Looking at these SQL Profiler results (averaged over 10 executions) we again see that the CTE actually gives us the best overall performance in terms of duration and resources used. The surprising thing in this case though is that the table variable outperformed the temporary table in all categories.

Query TypeReadsWritesCPUDuration (ms)
CTE10001
Temp table31811512
Table variable91101

As mentioned at the start of the article, a CTE is not quite as versatile as temporary tables and table variables, but we’ve shown here that for individual queries that require temporary/staged data it can be a good option to improve performance. Remember though when dealing with query performance to always test with your own queries/data as there are many variables that can affect performance and different use cases could lead to another option giving better performance.

Next Steps

6 Comments

  1. it seems to me the key issue is how well each of these options scales when dealing with megs of data. temp tables seem to scale very well

  2. The drop table should be removed for the performance testing. drop table will happen when the sql server has time, which means that statement it can be delayed. Tilting the performance time. Used to have the drop table at the end of stored procedures, but removed it as it is not require and performance increased.Would be interesting to see the performance without the drop table

  3. As a few people have mentioned before there are a lot of things that can effect the performance of your particular use case, it’s why I mention at the end of the tip you need to test to find out what would work best for your environment.
    While I agree with @Codeman and @Scott S that you could just select directly into the table (create the index after which is a good best practice when loading data) and it would be faster, that is not always the case. This simple example shows it’s better to create it before.

    SELECT customerid,max(orderdate) lastorderdate INTO #temptable
    FROM sales.SalesOrderHeader
    GROUP BY customerid;
    CREATE INDEX temptable_pk on #temptable (customerid);
    drop table #temptable
    GO

    –cpu 30ms, reads 1511, writes 81, duration 31ms

    CREATE TABLE #temptable (customerid [int] NOT NULL PRIMARY KEY,lastorderdate [datetime] NULL);
    INSERT INTO #temptable
    SELECT customerid,max(orderdate) lastorderdate FROM sales.SalesOrderHeader
    GROUP BY customerid;
    drop table #temptable
    GO

    –cpu 16ms, reads 1215, writes 33, duration 26ms

    The main point of the tips is to give people options and test what works/performs best for their particular use case.

  4. Codeman has the optimal solution. SELECT INTO is much, much quicker. And when you’re dealing with 100’s of 1000’s of rows you can measure that difference in hours.

  5. No need to create the temp table then insert into it. Just do a select into insead.

    SELECT customerid,max(orderdate) lastorderdate
    INTO #temptable
    FROM sales.SalesOrderHeader
    GROUP BY customerid;

  6. CTEs and table variables do not scale. If you can guarantee that you are dealing with small data sets, go crazy. If not, go with temp tables. There are million reasons why this works well for larger data sets.

Leave a Reply

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