SQL Server计划指南和其他非最佳做法

通常,有关查询优化的帖子会告诉您如何做正确的事情以帮助查询优化器选择最佳执行计划:在WHERE中使用SARGable表达式,仅检索所需的列,使用经过碎片整理并具有更新的统计信息的格式良好的索引。



今天,我想谈谈别的东西-绝不属于最佳实践的东西,借助这些东西,很容易使自己步履蹒跚,使先前执行的查询变慢,或者由于错误而不再执行...它是关于提示和计划指南的。



提示是查询优化器的提示,可以在MSDN上找到完整列表。其中一些实际上是提示(例如,您可以指定OPTION(MAXDOP 4)),以便可以在最大并行度= 4的情况下执行查询,但不能完全保证SQL Server会使用此提示生成并行计划。



另一部分是行动的直接指南。例如,如果您编写OPTION(HASH JOIN),则SQL Server将构建一个没有NESTED LOOPS和MERGE JOIN的计划。而且您知道如果事实证明仅凭哈希联接就不可能构建计划,那将会发生什么?优化器会这样说-我无法制定计划,并且查询将不会执行。



问题是(至少在我看来)某些提示是优化程序可以进入的提示并不为人所知。以及哪些提示是手动提示,如果出现问题,可能会导致请求崩溃。当然已经有一些现成的收藏品在其中进行描述,但这在任何情况下都不是官方信息,并且随时可能更改。



计划指南就是这样的事情(我不知道如何正确翻译),它使您可以将一组特定的提示绑定到一个特定的请求,您所知道的文本。例如,如果您不能直接影响由ORM生成的请求文本,则这可能是相关的。



提示和计划指南都绝不是最佳实践,而是省略提示和这些指南的优良作法,因为数据的分布可能会发生变化,数据类型可能会发生变化,因此有可能发生更多的事情。带提示的查询比没有提示的查询效果更差,并且在某些情况下将完全停止。您必须百分百知道自己在做什么以及为什么。



现在,简要说明为什么我什至参与其中。



我有一张宽桌子,上面有一堆不同大小的nvarchar字段-从10到最大。此表有很多查询,CHARINDEX可以在这些表中的一个或多个中搜索子字符串的出现。例如,有一个看起来像这样的请求:



SELECT *
FROM table
WHERE CHARINDEX(N' ', column)>1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET x ROWS FETCH NEXT y ROWS ONLY


该表在ID上具有聚集索引,在列上具有非唯一非聚集索引。如您所知,这一切都是零感觉,因为在WHERE中,我们使用CHARINDEX,这绝对是不可维护的。为了避免SB的潜在问题,我将在打开的数据库StackOverflow2013上模拟这种情况,可在此处找到



考虑dbo.Posts表,该表仅具有按ID组成的聚集索引和类似这样的查询:



SELECT *
FROM dbo.Posts
WHERE CHARINDEX (N'Data', Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY


为了匹配我的真实数据库,我在“标题”列上创建一个索引:



CREATE INDEX ix_Title ON dbo.Posts (Title);


结果,当然,我们得到了一个绝对合乎逻辑的执行计划,其中包括以相反的方向扫描聚簇索引:











而且,可以肯定,它执行得很好:

表“帖子”。扫描计数1,逻辑读516,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。SQL

Server执行时间:

CPU时间= 16毫秒


但是,如果我们寻找的是罕见的东西,而不是通用的“数据”,那会发生什么呢?例如N'Aptana'(不知道它是什么)。该计划当然会保持不变,但是执行统计数据会有所变化:

表“帖子”。扫描计数1,逻辑读253191,物理读113,预读224602,lob逻辑读0,lob物理读0,lob预读读0。SQL

Server执行时间:

CPU时间= 2563 ms


这也是合乎逻辑的-这个词不那么常见,SQL Server必须扫描更多数据才能找到25行。但不知何故,不是吗?

我正在创建一个非聚集索引。如果SQL Server使用它,也许会更好?他本人将不会使用它,因此我添加了一个提示:



SELECT *
FROM dbo.Posts
WHERE CHARINDEX (N'Aptana', Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY
OPTION (TABLE HINT (dbo.Posts, INDEX(ix_Title)));


而且,某种方式完全让人难过。执行统计:

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 'Posts'. Scan count 5, logical reads 109312, physical reads 5, read-ahead reads 104946, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

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.

SQL Server Execution Times:

CPU time = 35031 ms


和计划:







现在执行计划是并行的,它有两种,两种都在tempdb中溢出。顺便说一下,请注意第一种排序,该排序是在非聚集索引扫描之后,密钥查找之前执行的-这是一种特殊的SQL Server优化,旨在减少随机I / O的数量-密钥查找以聚集索引键的升序执行。您可以在此处了解更多信息



需要第二种排序以按降序ID选择25行。顺便说一句,SQL Server可能已经猜到它将不得不再次按ID排序,仅以降序排列,并在“相反”方向进行键查找,在开始时以降序排序,而不是以升序排序。



我没有提供查询执行的统计信息,该统计信息带有通过输入“数据”进行搜索的非聚集索引的提示。在笔记本电脑上半死的硬盘上,它花费了超过16分钟的时间,我没有想到要截屏。抱歉,我不想再等那么久了。

但是请求呢?聚集索引扫描是最终的梦想,而您做不到任何事情?



如果我想避免各种情况,我会想到并创建了一个非聚集索引,该索引通常与通常认为的非聚集索引的最佳做法相矛盾:



CREATE INDEX ix_Id_Title ON dbo.Posts (Id DESC, Title);


现在,我们使用提示告诉SQL Server使用它:



SELECT *
FROM dbo.Posts
WHERE CHARINDEX (N'Aptana', Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY
OPTION (TABLE HINT (dbo.Posts, INDEX(ix_Id_Title)));


哦,效果很好:





表“帖子”。扫描计数1,逻辑读6259,物理读0,预读7816,lob逻辑读0,lob物理读0,lob预读0。SQL

Server执行时间:

CPU时间= 1734 ms


处理器时间的增加不是很大,但是您需要读的少得多-还不错。频繁的“数据”呢?

表“帖子”。扫描计数1,逻辑读208,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。SQL

Server执行时间:

CPU时间= 0 ms


哇,那也很好。现在,由于请求来自ORM,我们无法更改其文本,因此我们需要弄清楚如何将该索引“钉”到请求中。计划指南就来了。



sp_create_plan_guide(MSDN存储过程用于创建计划指南



让我们详细考虑一下:



sp_create_plan_guide [ @name = ] N'plan_guide_name'  
    , [ @stmt = ] N'statement_text'  
    , [ @type = ] N'{ OBJECT | SQL | TEMPLATE }'  
    , [ @module_or_batch = ]  
      {   
        N'[ schema_name. ] object_name'  
        | N'batch_text'  
        | NULL  
      }  
    , [ @params = ] { N'@parameter_name data_type [ ,...n ]' | NULL }   
    , [ @hints = ] { 
                 N'OPTION ( query_hint [ ,...n ] )'   
                 | N'XML_showplan'  
                 | NULL 
      }  


名称 -清晰独特的计划指南名称

stmt-这是您需要向其添加提示的请求。重要的是要知道,此请求必须与来自应用程序的请求完全相同。奇怪的空间?计划指南将不被使用。换行错误?计划指南将不被使用。为了让您自己更轻松,这里有一个“生活技巧”,我稍后会再谈到(在这里找到)。



类型 -指示请求在 stmt... 如果它是存储过程的一部分,则应为OBJECT。如果这是来自多个请求的批处理的一部分,或者是临时请求,或者是来自一个请求的批处理,则应该有SQL。如果在此处指示TEMPLATE,则这是有关查询参数化的单独故事,您可以在MSDN上阅读该故事



@module_or_batch取决于类型... 如果一个类型='OBJECT',这应该是存储过程的名称。如果一个类型='BATCH'-应该有整个批处理的文本,逐字指定,并带有来自应用程序的内容。奇怪的空间?好吧,你已经知道了。如果为NULL,则我们认为这是来自一个请求的批处理,并且与中指定的匹配stmt 有所有限制。



参数-应该在此处列出所有传递给请求的参数以及数据类型。



最后,@ hints是个不错的部分,在这里您需要指定要添加到请求中的提示。在这里,您可以显式插入所需的XML格式的执行计划。此参数也可以为NULL,这将导致SQL Server将不使用查询中明确指定的提示的事实。stmt...



因此,我们为查询创建了一个计划指南:



DECLARE @sql nvarchar(max) = N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (N''Data'', Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY';

exec sp_create_plan_guide @name = N'PG_dboPosts_Index_Id_Title'  
    ,  @stmt =  @sql
    ,  @type = N'SQL'
    ,  @module_or_batch =  NULL          
    ,  @params = NULL
    ,  @hints =  N'OPTION (TABLE HINT (dbo.Posts, INDEX (ix_Id_Title)))'


我们尝试执行请求:

SELECT *
FROM dbo.Posts
WHERE CHARINDEX (N'Data', Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY


哇,它起作用了:







在最后一个SELECT语句的属性中,我们看到:







很好,已应用了计划方案。如果您现在搜索“ Aptana”怎么办?一切都会变得很糟-我们将再次返回对聚集索引的扫描,并说明所有后果。为什么?并且因为计划指南适用于SPECIFIC查询,该查询的文本与正在执行的查询一对一重合。



对我来说幸运的是,我系统上的大多数请求都已参数化。我没有处理非参数查询,但我希望不必这样做。对于他们来说,您可以使用模板(有关TEMPLATE的更多信息),可以在数据库中启用FORCED PARAMETERIZATION(不要在不了解自己正在做的事情的情况下进行!!!),然后在此之后,您就可以链接计划指南了。但是我真的没有尝试过。



在我的情况下,该请求以如下方式执行:



exec sp_executesql  
N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (@p0, Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY;'
, N'@p0 nvarchar(250), @p1 int, @p2 int'
, @p0 = N'Aptana', @p1 = 0, @p2 = 25;


因此,我创建了一个相应的计划指南:



DECLARE @sql nvarchar(max) = N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (@p0, Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY;';

exec sp_create_plan_guide @name = N'PG_paramters_dboPosts_Index_Id_Title'  
    ,  @stmt =  @sql
    ,  @type = N'SQL'
    ,  @module_or_batch =  NULL          
    ,  @params = N'@p0 nvarchar(250), @p1 int, @p2 int'
    ,  @hints =  N'OPTION (TABLE HINT (dbo.Posts, INDEX (ix_Id_Title)))'


而且,万岁,一切都可以按要求进行:











在温室条件之外,并非总是能够正确指定参数stmt将计划指南附加到请求上,为此,我在上面提到了一个“生活技巧”。我们清除计划缓存,删除指南,再次执行参数化查询,并从缓存中获取其执行计划和plan_handle。



例如,可以使用对此的请求:



SELECT qs.plan_handle, st.text, qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text (qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan (qs.plan_handle) qp






现在,我们可以使用sp_create_plan_guide_from_handle存储过程从现有计划中创建计划指南。



它以参数名称-创建的指南的名称,@plan_handle-现有执行计划的句柄,以及@statement_start_offset-定义应为其创建指南的批处理中语句的开头。



试:



exec sp_create_plan_guide_from_handle N'PG_dboPosts_from_handle'  
    , 0x0600050018263314F048E3652102000001000000000000000000000000000000000000000000000000000000
    , NULL;


现在,在SSMS中,我们将了解可编程性->计划指南中的功能:







现在已经使用计划指南'PG_dboPosts_from_handle'将当前执行计划“钉入”了我们的请求,但是,最重要的是,它现在几乎像任何对象一样在SSMS中,我们可以编写脚本并重新创建所需的方式。



RMB,脚本-> Drop and Create,我们得到了一个现成的脚本,其中我们需要将@hints参数的值替换为所需的脚本,因此得到:



USE [StackOverflow2013]
GO

/****** Object:  PlanGuide PG_dboPosts_from_handle    Script Date: 05.07.2020 16:25:04 ******/
EXEC sp_control_plan_guide @operation = N'DROP', @name = N'[PG_dboPosts_from_handle]'
GO
/****** Object:  PlanGuide PG_dboPosts_from_handle    Script Date: 05.07.2020 16:25:04 ******/
EXEC sp_create_plan_guide @name = N'[PG_dboPosts_from_handle]', @stmt = N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (@p0, Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY', @type = N'SQL', @module_or_batch = N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (@p0, Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY;', 
@params = N'@p0 nvarchar(250), @p1 int, @p2 int', 
@hints = N'OPTION (TABLE HINT (dbo.Posts, INDEX (ix_Id_Title)))'
GO


我们执行并重新执行该请求。Hooray,一切正常:







如果替换参数值,则一切相同。



请注意,只有一份指南可以对应一份声明。如果您尝试将其他指南添加到同一语句,您将收到一条错误消息。

消息10502,级别16,状态1,第1行

无法创建计划指南'PG_dboPosts_from_handle2',因为stmt和@module_or_batch,或通过@plan_handle和@statement_start_offset匹配数据库中现有的计划指南'PG_dboPosts_from_handle'。创建新的计划指南之前,请先删除现有的计划指南。


我最后要提到的是sp_control_plan_guide存储过程



在它的帮助下,您可以一次删除,禁用和启用计划指南-一次都指示名称,也要删除所有指南(我不确定-完全是什么。或者是执行该过程的数据库上下文中的所有内容)-为此使用了值@operation参数-DROP ALL,DISABLE ALL,ENABLE ALL。上面给出了使用HP进行特定计划的示例-删除了具有指定名称的特定计划指南。



是否有可能没有提示和计划指南?



通常,如果您觉得查询优化器是愚蠢的,并且正在玩某种游戏,并且您知道它是如何做到最好的,则有99%的可能性您正在做某种游戏(例如我的案例)。但是,在您无法直接影响请求文本的情况下,允许您向请求中添加提示的计划指南可能是可以的。假设我们有能力根据需要重写请求文本-可以更改某些内容吗?当然!即使不使用全文搜索形式的“异国情调”,实际上也应在此处使用。例如,这样的查询具有完全正常的(用于查询)计划和执行统计信息:



;WITH c AS (
    SELECT p2.id
    FROM dbo.Posts p2
    WHERE CHARINDEX (N'Aptana', Title) > 1
    ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
    OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY
)
SELECT p.* 
FROM dbo.Posts p
JOIN c ON p.id = c.id;




表“帖子”。扫描计数1,逻辑读6250,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。SQL

Server执行时间:

CPU时间= 1500毫秒


SQL Server首先通过ix_Id_Title“弯曲”索引找到所需的25个标识符,然后才使用所选标​​识符在聚集索引中进行搜索-甚至比指南更好!但是,如果我们对“数据”执行查询并显示从第20,000行开始的25行,会发生什么情况:



;WITH c AS (
    SELECT p2.id
    FROM dbo.Posts p2 
    WHERE CHARINDEX (N'Data', Title) > 1
    ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
    OFFSET 20000 ROWS FETCH NEXT 25 ROWS ONLY
)
SELECT p.* 
FROM dbo.Posts p
JOIN c ON p.id = c.id;




表“帖子”。扫描计数1,逻辑读5914,物理读0,预读0,lob逻辑读11,lob物理读0,lob预读0。SQL

Server执行时间:

CPU时间= 1453 ms


exec sp_executesql  
N'SELECT *
FROM dbo.Posts
WHERE CHARINDEX (@p0, Title) > 1
ORDER BY ROW_NUMBER() OVER (ORDER BY Id DESC)
OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY;'
, N'@p0 nvarchar(250), @p1 int, @p2 int'
, @p0 = N'Data', @p1 = 20000, @p2 = 25;




Table 'Posts'. Scan count 1, logical reads 87174, physical reads 0, read-ahead reads 0, lob logical reads 11, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:

CPU time = 1437 ms


是的,处理器时间是相同的,因为它花费在charindex上,但是指南的请求会多读取一个数量级,这可能会成为问题。



让我总结一下最终结果。提示和指南可以在此刻为您提供很多帮助,但它们很容易使事情变得更糟。如果您在请求文本中显式指定带有索引的提示,然后删除该索引,则根本无法执行查询。在我的SQL Server 2017上,删除索引后的带有指南的查询可以很好地执行-忽略了该指南,但是我不确定在所有版本的SQL Server中都将始终采用这种方式。



关于俄语的计划指南的信息很少,所以我决定自己编写。你可以在这里读关于使用计划指南的局限性,特别是有时使用PG提示对索引进行明确指示可能会导致请求下降的事实。我希望您永远不要使用它们,并且,如果您必须(祝您好运),您会知道这可能会导致什么。



All Articles