Hello again,
On my previous post, we covered how the Query Optimizer handles row estimation when using Table variables under specific conditions.
Following up on that, I will demonstrate other scenarios where the Query Optimizer must try to optimize queries when no statistics and histograms are available.
In the 2nd example of the series I will use a larger table in the AdventureWorks2012 database (Sales.SalesOrderDetail), and load it into a table variable. Note that I could just as easily have used the table in the previous post, which would yield the exact same practical result:
SET NOCOUNT ON;
DECLARE @tblSalesOrderDetail TABLE (
[SalesOrderID] [int] NOTNULL,
[CarrierTrackingNumber] [nvarchar](25) NULL,
[OrderQty] [smallint] NOTNULL,
[ProductID] [int] NOTNULL,
[SpecialOfferID] [int] NOTNULL,
[UnitPrice] [money] NOTNULL,
[UnitPriceDiscount] [money] NOTNULL);
INSERT INTO @tblSalesOrderDetail
SELECT [SalesOrderID]
,[CarrierTrackingNumber]
,[OrderQty]
,[ProductID]
,[SpecialOfferID]
,[UnitPrice]
,[UnitPriceDiscount]
FROM [AdventureWorks2012].[Sales].[SalesOrderDetail];
Then select from that table variable as follows:
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblSalesOrderDetail WHERE [CarrierTrackingNumber] LIKE'5A1A-4E3D-B2'
OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblSalesOrderDetail WHERE [ProductID] LIKE''
OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
Moving on to the 3rd example, with the BETWEEN operator, take the following query:
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblSalesOrderDetail WHERE [ProductID] BETWEEN 700 AND 800
OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;

In this context with a single BETWEEN operator, 10918.53 rows are exactly 9 percent of the table variable cardinality (10918.53 * 100 / 121317 = 9).
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblSalesOrderDetail WHERE [ProductID] NOT BETWEEN 700 AND 800
OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
Another interesting estimation scenario shown in my 4th example involves BIT type search arguments. For this I will use the HumanResources.Employee table again, and load it into a table variable:
SET NOCOUNT ON;
DECLARE @tblEmployee TABLE (
[NationalIDNumber] [nvarchar](15) NOTNULL,
[LoginID] [nvarchar](256) NOTNULL,
[OrganizationNode] [hierarchyid] NULL,
[JobTitle] [nvarchar](50) NOTNULL,
[BirthDate] [date] NOTNULL,
[MaritalStatus] [nchar](1) NOTNULL,
[Gender] [nchar](1) NOTNULL,
[HireDate] [date] NOTNULL,
[SalariedFlag] bitNOTNULL,
[VacationHours] [smallint] NOTNULL,
[SickLeaveHours] [smallint] NOTNULL);
INSERT INTO @tblEmployee
SELECT [NationalIDNumber]
,[LoginID]
,[OrganizationNode]
,[JobTitle]
,[BirthDate]
,[MaritalStatus]
,[Gender]
,[HireDate]
,[SalariedFlag]
,[VacationHours]
,[SickLeaveHours]
FROM [AdventureWorks2012].[HumanResources].[Employee];
Then, search on the SalariedFlag column as follows:
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblEmployee WHERE [SalariedFlag]=1 OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
This is because by design, when a BIT column is searched on (remember that SalariedFlag does not allow NULLs), it can only have 2 known values (true or false). Therefore, because there are no histograms to support an accurate estimation, the Query Optimizer estimates 50% of rows for either value.
To further demonstrate this point, if we change the search argument in the same query to:
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblEmployee WHERE [SalariedFlag]=0 OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
The Estimated Rowcount is still 145 while the Actual Rowcount is 238.
Notice I explicitly stated that the SalariedFlag does not allow NULLs. What if it did?
Let’s recreate the table variable allowing NULLs in the very same column, and load it into a table variable:
SET NOCOUNT ON;
DECLARE @tblEmployee TABLE (
[NationalIDNumber] [nvarchar](15) NOTNULL,
[LoginID] [nvarchar](256) NOTNULL,
[OrganizationNode] [hierarchyid] NULL,
[JobTitle] [nvarchar](50) NOTNULL,
[BirthDate] [date] NOTNULL,
[MaritalStatus] [nchar](1) NOTNULL,
[Gender] [nchar](1) NOTNULL,
[HireDate] [date] NOTNULL,
[SalariedFlag] bitNULL,
[VacationHours] [smallint] NOTNULL,
[SickLeaveHours] [smallint] NOTNULL);
INSERT INTO @tblEmployee
SELECT [NationalIDNumber]
,[LoginID]
,[OrganizationNode]
,[JobTitle]
,[BirthDate]
,[MaritalStatus]
,[Gender]
,[HireDate]
,[SalariedFlag]
,[VacationHours]
,[SickLeaveHours]
FROM [AdventureWorks2012].[HumanResources].[Employee];
Then, search on the SalariedFlag column just as we did before:
SETSTATISTICS PROFILE ON;
SELECT * FROM @tblEmployee WHERE [SalariedFlag]=1 OPTION(RECOMPILE)
SETSTATISTICS PROFILE OFF;
For a table cardinality of 290, 95.7 rows are exactly 33 percent of that (95.7* 100 / 290 = 33).
Keeping in mind that SalariedFlag now allows NULLs, it can have 3 known values (true, false or NULL). Therefore, and in a scenario where there are no histograms, the Query Optimizer estimates 33% of rows for each possible value. Note that searching on NULL values in the same query would yield 0 as the Actual Rowcount, although the Estimated Rowcount is still 33%.
That’s it for now, hope you will take all this into account when dealing with table variables, as these are most valuable tools for several usage scenarios. You can explorer further starting with the Frequently Asked Questions - SQL Server 2000 - Table Variables and Query performance and table variables.
See you next time!
Disclaimer: I hope that the information on these pages is valuable to you. Your use of the information contained in these pages, however, is at your sole risk. All information on these pages is provided "as -is", without any warranty, whether express or implied, of its accuracy, completeness, fitness for a particular purpose, title or non-infringement, and none of the third-party products or information mentioned in the work are authored, recommended, supported or guaranteed by Ezequiel. Further, Ezequiel shall not be liable for any damages you may sustain by using this information, whether direct, indirect, special, incidental or consequential, even if it has been advised of the possibility of such damages.