Troubleshooting Poor Query Performance: Constant Folding and Expression Evaluation During Cardinality Estimation
SQL Server evaluates some constant expressions early to improve query performance. This is referred to as constant folding. A constant is a Transact-SQL literal, such as 3, 'ABC', '2005-12-31', 1.0e3, or 0x12345678.
SQL Server uses constant folding with the following types of expressions:
Arithmetic expressions, such as 1+1, 5/3*2, that contain only constants.
Logical expressions, such as 1=1 and 1>2 AND 3>4, that contain only constants.
Built-in functions that are considered foldable by SQL Server, including CAST and CONVERT. Generally, an intrinsic function is foldable if it is a function of its inputs only and not other contextual information, such as SET options, language settings, database options, and encryption keys. Nondeterministic functions are not foldable. Deterministic built-in functions are foldable, with some exceptions.
An exception is made for large object types. If the output type of the folding process is a large object type (text, image, nvarchar(max), varchar(max), or varbinary(max)), then SQL Server does not fold the expression.
All other expression types are not foldable. In particular, the following types of expressions are not foldable:
Nonconstant expressions such as an expression whose result depends on the value of a column.
Expressions whose results depend on a local variable or parameter, such as @x.
User-defined functions (both Transact-SQL and CLR).
Expressions whose results depend on language settings.
Expressions whose results depend on SET options.
Expressions whose results depend on server configuration options.
Examples of Foldable and Nonfoldable Constant Expressions
Consider the following query:
SELECT * FROM Sales.SalesOrderHeader s JOIN sales.SalesOrderDetail d ON s.SalesOrderID = d.SalesOrderID WHERE TotalDue > 117.00 + 1000.00
If the PARAMETERIZATION database option is not set to FORCED for this query (the default is SIMPLE), then the expression 117.00 + 1000.00 is evaluated and replaced by its result, 1117.00, before the query is compiled. Benefits of this constant folding include the following:
The expression does not have to be evaluated repeatedly at run time.
The value of the expression after it is evaluated is used by the query optimizer to estimate the size of the result set of the portion of the query TotalDue > 117.00 + 1000.00.
On the other hand, if dbo.f is a scalar user-defined function, the expression dbo.f(100) is not folded, because SQL Server does not fold expressions that involve user-defined functions, even if they are deterministic.
In addition, some expressions that are not constant folded but whose arguments are known at compile time, whether the arguments are parameters or constants, are evaluated by the result-set size (cardinality) estimator that is part of the optimizer during optimization.
Specifically, the following built-in functions and special operators are evaluated at compile time if all their inputs are known: UPPER, LOWER, RTRIM, DATEPART( YY only ), GetDate, CAST, and CONVERT.
The following operators are also evaluated at compile time if all their inputs are known:
Arithmetic operators: +, -, *, /, unary -,
Logical Operators: AND, OR, NOT
Comparison operators: <, >, <=, >=, <>, LIKE, IS NULL, IS NOT NULL
No other functions or operators are evaluated by the optimizer during cardinality estimation.
Examples of Compile-Time Expression Evaluation
Consider this Transact-SQL stored procedure:
USE AdventureWorks2008R2; GO CREATE PROCEDURE MyProc( @d datetime ) AS SELECT COUNT(*) FROM Sales.SalesOrderHeader WHERE OrderDate > @d+1;
During optimization of the SELECT statement in the procedure, the optimizer tries to evaluate the expected cardinality of the result set for the condition OrderDate > @d+1. The expression @d+1 is not constant-folded, because @d is a parameter. However, at optimization time, the value of the parameter is known. This allows the optimizer to accurately estimate the size of the result set, which helps it select a good query plan.
Now consider an example similar to the previous one, except that a local variable @d2 replaces @d+1 in the query and the expression is evaluated in a SET statement instead of in the query.
USE AdventureWorks2008R2; GO CREATE PROCEDURE MyProc2( @d datetime ) AS BEGIN DECLARE @d2 datetime SET @d2 = @d+1 SELECT COUNT(*) FROM Sales.SalesOrderHeader WHERE OrderDate > @d2 END;
When the SELECT statement in MyProc2 is optimized in SQL Server, the value of @d2 is not known. Therefore, the optimizer uses a default estimate for the selectivity of OrderDate > @d2, (in this case 30 percent).
To make sure you generate optimal query plans, it is best to design queries, stored procedures, and batches so that the query optimizer can accurately estimate the selectivity of the conditions in your query, based on statistics about your data distribution. Otherwise, the optimizer must use a default estimate when estimating selectivity (such as 30 percent in the previous example).
To make sure that the cardinality estimator of the optimizer provides good estimates, you should first make sure that the AUTO_CREATE_STATISTICS and AUTO_UPDATE_STATISTICS database SET options are ON (the default setting), or that you have manually created statistics on all columns referenced in a query condition. Then, when you are designing the conditions in your queries, do the following when it is possible:
Avoid the use of local variables in queries. Instead, use parameters, literals, or expressions in the query.
Limit the use of operators and functions embedded in a query that contains a parameter to those listed under Compile-Time Expression Evaluation for Cardinality Estimation.
Make sure that constant-only expressions in the condition of your query are either constant-foldable, or can be evaluated at compilation time.
If you have to use a local variable to evaluate an expression to be used in a query, consider evaluating it in a different scope than the query. For example, it may be helpful to perform one of the following:
Pass the value of the variable to a stored procedure that contains the query you want to evaluate, and have the query use the procedure parameter instead of a local variable.
Construct a string that contains a query based in part on the value of the local variable, and then execute the string by using dynamic SQL (EXEC or sp_executesql).
Parameterize the query and execute it by using sp_executesql, and pass the value of the variable as a parameter to the query.