SQL Server – How Table Variable Improves Query Performance

performancequery-performancesql serversql server 2014table variabletemporary-tables

for this specific case, that I will try to explain below, using a table variable is performing better than not using a table variable.

I would like to know why, and if possible, get rid of the table variable.

this is the query using the table variable:

USE [BISource_UAT]
GO

set statistics io on
SET STATISTICS TIME ON

    SET NOCOUNT ON;

    DECLARE @OrderStartDate DATETIME = '15-feb-2015'
    DECLARE @OrderEndDate DATETIME = '28-feb-2016'
    DECLARE @tmp TABLE
    (
    strBxOrderNo VARCHAR(20)
    ,sintReturnId INT
    )  

    INSERT INTO @tmp
    SELECT  strBxOrderNo
            ,sintReturnId
    FROM    TABLEBACKUPS.dbo.tblBReturnHistory rh
    WHERE   rh.sintReturnStatusId in ( 3 )
    AND     rh.dtmAdded >= @OrderStartDate
    AND     rh.dtmAdded < @OrderEndDate

    SELECT 
             op.lngPaymentID
            ,op.strBxOrderNo
            ,op.sintPaymentTypeID
            ,op.strCurrencyCode
            ,op.strBCCurrencyCode
            ,op.decPaymentAmount
            ,op.decBCPaymentAmount
            ,ap.strAccountCode
            ,o.sintMarketID
            ,o.sintOrderChannelID
            ,o.sintOrderTypeID
            ,CASE   WHEN opgv.lngpaymentID IS NULL THEN NULL
                     -- Not a Voucher = Null
                WHEN gvp.strIssuedBxOrderNo IS NULL THEN 0 ELSE 1 
              END AS [IsPromoVoucher] -- Is a Voucher - check type
            ,o.sdtmOrdCreated

    FROM    @tmp rh

            INNER JOIN TABLEBACKUPS.dbo.tblBReturn r 
                    ON r.sintReturnId = rh.sintReturnId 
                   AND r.strBxOrderNo = rh.strBxOrderNo

            INNER JOIN bocss2.dbo.tblBOrder o 
                    ON o.strBxOrderNo = r.strBxOrderNo

            INNER JOIN Bocss2.dbo.tblBOrderPayment op 
                    ON op.strBxOrderNo = o.strBxOrderNo

            INNER JOIN TABLEBACKUPS.dbo.tblBOrderItemReturn AS oir 
                    ON r.sintReturnId = oir.sintReturnID 
                   AND r.strBxOrderNo = oir.strBxOrderNo

            INNER JOIN Bocss2.dbo.tblBOrderItem AS i 
                    ON i.strBxOrderNo = oir.strBxOrderNo 
                   AND i.sintOrderSeqNo = oir.sintOrderSeqNo

            INNER JOIN TABLEBACKUPS.dbo.tblBAccountParticipant ap 
                   ON o.lngAccountParticipantID = ap.lngParticipantID

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBOrderPaymentGiftVoucher opgv 
                         ON op.lngPaymentID = opgv.lngPaymentID

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBGiftVoucher gv 
                         ON opgv.strVoucherNumber = gv.strVoucherNumber

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBGiftVoucherPromotion gvp 
                         ON gvp.strIssuedBxOrderNo = gv.strIssuedBxOrderNo

    WHERE   oir.decReturnFinalAmount > 0
    AND     o.sdtmOrdCreated >= @OrderStartDate

this produces the following stats:

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 78 ms, elapsed time = 86 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
Table '#BF0B2154'. Scan count 0, logical reads 1957, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBReturnHistory'. Scan count 1, logical reads 13, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 9 ms.
Table 'tblBGiftVoucherPromotion'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBGiftVoucher'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderPaymentGiftVoucher'. Scan count 0, logical reads 452, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderItem'. Scan count 0, logical reads 904, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderPayment'. Scan count 186, logical reads 1649, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBAccountParticipant'. Scan count 0, logical reads 7112, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrder'. Scan count 3557, logical reads 14267, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderItemReturn'. Scan count 1951, logical reads 5865, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBReturn'. Scan count 0, logical reads 3902, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#BF0B2154'. Scan count 1, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 125 ms,  elapsed time = 138 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

using showplan_text on I would like to show the query plan:

first part of the query – populating the table variable
enter image description here

second part of the query:
using the table varible and joining the other tables:
enter image description here

This is the XML plan of the query using table variable.

now this is the same query NOT using a table variable:

USE [BISource_UAT]
GO

set statistics io on
SET STATISTICS TIME ON

    SET NOCOUNT ON;

    DECLARE @OrderStartDate DATETIME = '15-feb-2015'
    DECLARE @OrderEndDate DATETIME = '28-feb-2016'

    SELECT 
             op.lngPaymentID
            ,op.strBxOrderNo
            ,op.sintPaymentTypeID
            ,op.strCurrencyCode
            ,op.strBCCurrencyCode
            ,op.decPaymentAmount
            ,op.decBCPaymentAmount
            ,ap.strAccountCode
            ,o.sintMarketID
            ,o.sintOrderChannelID
            ,o.sintOrderTypeID
            ,CASE   WHEN opgv.lngpaymentID IS NULL 
               THEN NULL -- Not a Voucher = Null
                WHEN gvp.strIssuedBxOrderNo IS NULL 
                THEN 0 ELSE 1 END AS [IsPromoVoucher] 
                -- Is a Voucher - check type
            ,o.sdtmOrdCreated

    FROM    TABLEBACKUPS.dbo.tblBReturnHistory rh

            INNER JOIN TABLEBACKUPS.dbo.tblBReturn r 
                    ON r.sintReturnId = rh.sintReturnId 
                   AND r.strBxOrderNo = rh.strBxOrderNo

            INNER JOIN bocss2.dbo.tblBOrder o 
                    ON o.strBxOrderNo = r.strBxOrderNo
                   AND o.sdtmOrdCreated >= @OrderStartDate

            INNER JOIN Bocss2.dbo.tblBOrderPayment op 
                    ON op.strBxOrderNo = o.strBxOrderNo

            INNER JOIN TABLEBACKUPS.dbo.tblBOrderItemReturn AS oir 
                    ON r.sintReturnId = oir.sintReturnID 
                   AND r.strBxOrderNo = oir.strBxOrderNo
                   AND oir.decReturnFinalAmount > 0

            INNER JOIN Bocss2.dbo.tblBOrderItem AS i 
                    ON i.strBxOrderNo = oir.strBxOrderNo 
                   AND i.sintOrderSeqNo = oir.sintOrderSeqNo

            INNER JOIN TABLEBACKUPS.dbo.tblBAccountParticipant ap 
                   ON o.lngAccountParticipantID = ap.lngParticipantID

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBOrderPaymentGiftVoucher opgv 
                         ON op.lngPaymentID = opgv.lngPaymentID

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBGiftVoucher gv 
                         ON opgv.strVoucherNumber = gv.strVoucherNumber

            LEFT OUTER JOIN TABLEBACKUPS.dbo.tblBGiftVoucherPromotion gvp 
                         ON gvp.strIssuedBxOrderNo = gv.strIssuedBxOrderNo

    WHERE   rh.sintReturnStatusId in ( 3 )
    AND     rh.dtmAdded >= @OrderStartDate
    AND     rh.dtmAdded < @OrderEndDate

when having a look at the stats, this is what we have got:

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBGiftVoucher'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBAccountParticipant'. Scan count 1, logical reads 32, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBReturn'. Scan count 1, logical reads 170, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderItemReturn'. Scan count 0, logical reads 35849, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderPayment'. Scan count 9408, logical reads 87643, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderItem'. Scan count 1950, logical reads 8336, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrder'. Scan count 1951, logical reads 7835, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBReturnHistory'. Scan count 1, logical reads 13, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBOrderPaymentGiftVoucher'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'tblBGiftVoucherPromotion'. Scan count 1, logical reads 27, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 625 ms,  elapsed time = 612 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Now, regarding the execution plan in text format:

setting the parameters

enter image description here

Now the important part, running the show:
enter image description here

This is the XML plan of the query NOT using table variable.

But how come using the table variable I got less reads, less I/O, and the execution (without clearing the cache) has always been faster?

I can provide any create table script, or anything else necessary for better understanding of this situation.

just post any comment here and I will reply.

this is a similar question:

Why is using a table variable more than twice as fast as a #temp table in this specific case?

when running the queries after CHECKPOINT; DBCC DROPCLEANBUFFERS;
the results were:

query with table variable

query with table variable

query without table variable

query without table variable

Best Answer

The main factors in play here are:

  • The optimizer does not try to find the best plan; its goal is to find a reasonable plan quickly
  • It assumes the query will be run with a cold cache
  • The cost model used favours sequential I/O over random I/O
  • Repeated seeks into an index are assumed to be randomly distributed

The cardinality estimate for a table variable is 1 row (unless a statement level recompile occurs, or trace flag 2453 is active). This low estimate results in a very low-cost plan, featuring a navigational strategy based on nested loops. This plan may continue to be effective for relatively low row counts, especially if the data needed does not need to be read in from persistent storage.

With more accurate cardinality estimates, the optimizer favours a plan using hash joins and a few scans. This appears to be cheaper than a navigational strategy, given the assumptions listed above; especially concerning the cold cache, and the relatively low cost of a sequential scan compared with many seeks (assuming a largely random I/O pattern).

The table variable plan may be slower than the alternative if the data needed is not in memory - or it may not. The cost model is exactly that - a model - the exact numbers used may not be representative of your hardware and configuration, and the assumptions made may not be valid in the particular circumstances.

All these caveats apply especially to low cost queries (which both are) since small cost changes can produce very different plan shapes. In fact, both plans are successful in that they produce results quickly and efficiently enough.