SQL Server 2016 Developer's Guide
上QQ阅读APP看书,第一时间看更新

New query hints

The SQL Server Query Optimizer does an amazing job of execution plan generation. Most of the time, for most queries, it generates an optimal execution plan. And this is not easy at all. There is a lot of potential to get a suboptimal plan: wrong server configuration, poorly designed databases, missing and suboptimal indexes, suboptimal written queries, nonscalable solutions, and so on. And the query optimizer should work for all those workloads, all over the world, all the time.

Depending on data constellation, it generates suboptimal execution plans sometimes. If the execution of the queries is very important from a business point of view, you have to do something to try to achieve at least an acceptable execution plan. One of the weapons you have for this is hints to the query optimizer. With hints, which are actually instructions, you instruct the query optimizer how to generate the execution plan. You take responsibility or you take part of the responsibility for the execution plan generation.

There are three types of hints: table, join, and query hints. You use them if you cannot enforce the required execution plan with other actions. Hints should be considered as a last resort in query tuning. You should use them if you don't know another way to get a desired plan, when you have tried all that you know, or when you are under time pressure and have to fix a query as soon as possible. For instance, if you suddenly encounter a problem on the production system on the weekend or during the night, you can use the hint as a temporary solution or a workaround and then look for a definitive solution without time or operative pressures. Literally said, you can use query hints if you know what you are doing. However, you should not forget that the hint and the plan remain forever and you need to periodically evaluate whether the plan is still adequate. In most cases, developers forget about plans with hints as soon as the performance problem is gone.

SQL Server 2016 brings three new query hints to address problems related to memory grants and performance spools:

  • NO_PERFORMANCE_SPOOL
  • MAX_GRANT_PERCENT
  • MIN_GRANT_PERCENT

NO_PERFORMANCE_SPOOL

The new query hint NO_PERFORMANCE_SPOOL has been added to SQL Server 2016 to allow users to enforce an execution plan, which does not contain a spool operator.

A spool operator in an execution plan does not mean that the plan is suboptimal; it is usually a good choice of query optimizer. However, in some cases, it can reduce the overall performance. This happens, for instance, when a query or a stored procedure whose execution plan contains a spool operator is executed by numerous parallel connections. Since the Spool operator uses tempdb, this can lead to tempdb contention when many queries are running at the same time. Using this hint, you can avoid this issue.

To demonstrate the use of this query hint, you will once again use the new WideWorldImporters sample database. Assume that you need to display order details for orders picked up by the sales persons provided in an input list. To ensure that the execution plan contains a spool operator, this example uses a user-defined function and not the new SQL Server 2016 function STRING_SPLIT. Use the following code to create the function and filter orders with the sales persons from the list.

USE WideWorldImporters; 
GO 
CREATE OR ALTER FUNCTION dbo.ParseInt 
( 
   @List       VARCHAR(MAX), 
   @Delimiter  CHAR(1) 
) 
RETURNS @Items TABLE 
( 
   Item INT 
) 
AS 
BEGIN 
   DECLARE @Item VARCHAR(30), @Pos  INT; 
   WHILE LEN(@List)>0 
   BEGIN 
       SET @Pos = CHARINDEX(@Delimiter, @List); 
       IF @Pos = 0 SET @Pos = LEN(@List)+1; 
       SET @Item = LEFT(@List, @Pos-1); 
       INSERT @Items SELECT CONVERT(INT, LTRIM(RTRIM(@Item))); 
       SET @List = SUBSTRING(@List, @Pos + LEN(@Delimiter), LEN(@List)); 
       IF LEN(@List) = 0 BREAK; 
   END 
   RETURN; 
END 
GO 
DECLARE @SalesPersonList VARCHAR(MAX) = '3,6,8'; 
SELECT o.* 
FROM Sales.Orders o 
INNER JOIN dbo.ParseInt(@SalesPersonList,',') a ON a.Item = o.SalespersonPersonID  
ORDER BY o.OrderID; 

When you observe the execution plan for this query, you can see the Table Spool operator in it, as Figure 4.4 shows:

Figure 4.4: Execution plan with the Table Spool operator

When you execute exactly the same query, but with the query hint OPTION (NO_PERFORMANCE_SPOOL), you get a different execution plan, without the Spool operator, as shown in Figure 4.5:

Figure 4.5: Execution plan without the Table Spool operator

By observing the execution plan, you can see that the spool operator has disappeared from it. You can also see that the Estimated Subtree Cost is about 10% higher for the plan without the hint (by comparing the yellow SELECT property boxes); therefore, the query optimizer has chosen the original plan with the Spool operator. Here, you used the hint just for demonstration purposes to show that you can enforce another plan, without the spool operator.

Note

You have to create a user-defined function in this example because this query with a new STRING_SPLIT function has an execution plan without the Spool operator.

However, if the Spool operator is required in an execution plan to enforce the validity and correctness, the hint will be ignored. To demonstrate this behavior, you will use the next example. First, you need to create a sample table and insert two rows into it:

USE WideWorldImporters; 
DROP TABLE IF EXISTS dbo.T1 
CREATE TABLE dbo.T1( 
id INT NOT NULL, 
c1 INT NOT NULL, 
); 
GO 
INSERT INTO dbo.T1(id, c1) VALUES(1, 5),(1, 10); 

Now, assume that you want to add some of the existing rows into the table, say the rows where ID has value less than 10. At this point, only one row qualifies for this insert. The following query implements this requirement:

INSERT INTO dbo.T1(id, c1) 
SELECT id, c1 FROM dbo.T1 
WHERE id < 10; 

The execution plan for this query is shown in Figure 4.6:

Figure 4.6: Execution plan for INSERT statement with the Table Spool operator

When you observe it, you can see the Table Spool operator proudly staying in the middle of the execution plan. However, when you execute the same statement with the NO_PERFORMANCE_SPOOL hint, you get an identical execution plan; the query hint is simply ignored. The reason for this decision by the query optimizer is that the spool operator in this plan is used not for optimization, but to guarantee the correctness of the result. To demonstrate this, execute these two statements:

INSERT INTO dbo.T1(id, c1) 
SELECT id, c1 FROM dbo.T1 
WHERE id < 10; 
 
INSERT INTO dbo.T1(id, c1) 
SELECT id, c1 FROM dbo.T1 
WHERE id < 10 
OPTION (NO_PERFORMANCE_SPOOL); 

Figure 4.7 shows both plans and it is obvious that this is the same execution plan:

Figure 4.7: Execution plans showing that the hint NO_PERFORMANCE_SPOOL is ignored

Use the query hint NO_SPOOL_OPERATOR when:

  • You want to avoid the spool operator in the execution object
  • You know that this is a good idea (performance issue is caused by the Spool operator)
  • You cannot achieve this, with a reasonable effort otherwise

MAX_GRANT_PERCENT

The MIN_GRANT_PERCENT and MAX_GRANT_PERCENT hints were first introduced in the SQL Server 2012 SP3 and now in SQL Server 2016 RTM (they are still not available in SQL Server 2014). They address the problem of inappropriate memory grant for query execution.

Memory grant is a memory associated for the execution of queries whose execution plan contains operators that need to store temporary row data while sorting and joining rows (Sort, Hash Join, and so on). The value for memory grant depends on SQL Server's Estimated Number of Rows that should be processed by memory operators. If the estimated number of rows significantly differs from the actual, the memory grant is overestimated or underestimated.

To demonstrate an overestimated memory grant, use the following code that generates a 10M row large table:

USE WideWorldImporters; 
DROP TABLE IF EXISTS dbo.T1; 
CREATE TABLE dbo.T1( 
id INT NOT NULL, 
c1 INT NOT NULL, 
c2 TINYINT NOT NULL DEFAULT 1, 
c3 CHAR(100) NOT NULL DEFAULT 'test', 
CONSTRAINT PK_T1 PRIMARY KEY CLUSTERED (id ASC) 
); 
GO 
INSERT INTO dbo.T1(id, c1) 
SELECT  
  n AS id, 
  1 + ABS(CHECKSUM(NEWID())) % 10000 AS c1 
FROM dbo.GetNums(10000000); 
GO 
CREATE INDEX ix1 ON dbo.T1(c2); 
GO 

Note that the c2 column has a value 1 for all the rows in the table. Also, note that the code sample uses the GetNums function introduced at the beginning of this chapter. Assume that your task is to return rows from the T1 table, where the c2 column has value the 0 or 2. The query is very simple:

SELECT * FROM dbo.T1 WHERE c2 IN (0, 2) ORDER BY c1; 

The query returns no rows (as mentioned, c2 = 1 for all rows in the table). The execution plan is simple too: Clustered Index Scan followed by the Sort operator. The plan is shown in Figure 4.8:

Figure 4.8: Execution plan with an overestimated memory grant

You can see that the query optimizer has significantly overestimated the number of rows for the Sort operator. Therefore, it is the most expensive part of the execution plan, even in concurrency with the Clustered Index Scan of a 10M row table. Actually, the result of the query is an empty set, but SQL Server thinks that all rows from the table will be returned. This is a bug with new cardinality in SQL Server 2014 and it is not fixed in SQL Server 2016. Since the Sort operator requires memory, this query needs a memory grant. With the mouse over the Select operator, you can see that the memory granted for this query is 1.9 GB. More details about memory grant are available in the XML representation of the execution plan, as shown in Figure 4.9:

Figure 4.9: Memory Grant information in the XML representation of the execution plan

For the execution of a single query that returns no rows, about 2 GB memory is granted! If this query was executed by 100 concurrent sessions, almost 200 GB of memory would be granted for its execution.

As mentioned earlier, query hints should be used as a last resort for performance tuning. For demonstration purposes, you will use the MAX_ GRANT_PERCENT query hint to limit the amount of memory granted.

The MAX_GRANT_PERCENT query hint defines the maximum memory grant size as a percentage of available memory. It accepts float values between 0.0 and 100.0. Granted memory cannot exceed this limit, but can be lower if the resource governor setting is lower than this. Since you know that you have no rows in the output, you can use a very low value in the query hint:

SELECT * FROM dbo.T1 WHERE c2 IN (0, 2) ORDER BY c1 OPTION (MAX_GRANT_PERCENT=0.001); 

When you observe the execution plan shown in Figure 4.10, you can see that the plan remains the same, only the memory grant value has dropped to 512 KB!

Figure 4.10: Execution plan with the query hint MAX_GRANT_PERCENT

With the memory grant hint, you cannot change the execution plan, but you can significantly reduce the amount of memory granted.

Note

All execution plans shown in this section were generated by a database engine with the SQL Server 2016 RTM installed on it. If you have installed SQL Server 2016 Service Pack 1, you will see a new "Excessive Grant" warning in the SELECT property box, indicating a discrepancy between granted and used memory for the query execution. You can find more details about this warning at https://support.microsoft.com/en-us/help/3172997.

You can see that the applied query hint saved 2 GB of memory. The query hint MAX_GRANT_PERCENT should be used when:

  • You don't know how to trick the optimizer into coming up with the appropriate memory grant
  • You want to fix the problem immediately and to buy time to search for a final solution

Here, you have used the query hint just for demonstration purposes, the correct way to fix a query is to understand why the estimated and actual number of rows are so discrepant and to then try to rewrite the query. As mentioned earlier, in this case, the problem is caused by the change in the Cardinality Estimator (CE) in the SQL Server 2014 and the query works well under the old CE. Use the following query to force SQL Server to use the old CE using the trace flag 9481, as shown in the following query:

SELECT * FROM dbo.T1 WHERE c2 IN (0, 2) ORDER BY c1 OPTION (QUERYTRACEON 9481); 

This generates the execution plan displayed in Figure 4.11:

Figure 4.11: Execution plan where the old cardinality estimator is enforced

You can see the expected Nested Loop Join operator and a symbolic memory grant of 1 MB. The old CE estimates one row to be returned, while the new CE estimates the whole table. These very different assumptions lead to completely different execution plans and memory grants.

Another option to fix this problem is to rewrite the query to use UNION ALL instead of the IN operator. Let's rewrite our original query:

SELECT * FROM dbo.T1 WHERE c2 = 0 
UNION ALL 
SELECT * FROM dbo.T1 WHERE c2 = 2 
ORDER BY c1; 

Figure 4.12 shows the execution plan for the preceding query:

Figure 4.12: Execution plan for the rewritten query

You can see that the plan is a double of the old CE but significantly better than the initial one. With the MAX_GRANT_PERCENT query hint, you have successfully reduced the amount of granted memory. However, as demonstrated, you could even improve the initial execution plan by using other techniques. Therefore, use the query hint only when you don't know how to instruct the query optimizer to generate a better estimation.

MIN_GRANT_PERCENT

Similar to the previous example, in the event of a memory underestimation, you can force the optimizer to guarantee a minimum memory grant for a query execution using the MIN_GRANT_PERCENT query hint. It also accepts float values between 0.0 and 100.0. Be very careful with this hint, since if you specify 100 percent as the minimum grant, the whole resource governor memory will be granted to a single query execution. If you specify more memory than is available, the query will simply wait for memory grant.