如何衡量或找到创建查询计划的成本?


18

我有一个典型的情况,参数嗅探会导致“不良”执行计划进入计划缓存,从而导致存储过程的后续执行非常缓慢。我可以使用局部变量OPTIMIZE FOR ... UNKNOWN和来“解决”此问题OPTION(RECOMPILE)。但是,我也可以深入查询并尝试对其进行优化。

我正在尝试确定是否应该:在有限的时间内解决问题,我想知道这样做的代价。如我所见,如果我坚持使用OPTION(RECOMPILE),最终结果是每次运行查询时都会重新创建查询计划。所以,我想我需要知道:

如何找出创建查询计划的成本是多少?

为了回答我自己的问题,我已经使用Googled进行了查询(例如,使用该查询),并浏览了dm_exec_query_statsDMV列的文档 。我还检查了SSMS中的“实际查询计划”的输出窗口以查找此信息。最后,我已经搜查DBA.SE。这些都没有一个答案。

谁能告诉我?是否可以找到或衡量计划创建所需的时间?


5
我建议获取Benjamin Nevarez提供Inside the SQL Server Query Optimizer的副本。免费。第5章“优化过程”可以帮助您计算出查询的编译时间。至少,它提供了有关优化程序创建查询计划所需要的信息。
马克·辛金森

Answers:


18

如何找出创建查询计划的成本是多少?

您可以查看查询计划中根节点的属性,例如:

根属性提取
(来自免费的Sentry One Plan Explorer的屏幕截图)

也可以通过查询计划缓存来获得此信息,例如使用基于以下关系的查询:

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

结果片段

有关处理这类查询的选项的完整说明,请参见Erland Sommarskog 最近更新的文章


4

假设“成本”以时间为单位(尽管不确定;-),那么至少,您应该可以通过执行以下操作来了解它:

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

在“消息”选项卡中报告的第一项应为:

SQL Server解析和编译时间:

我将至少运行10次,并平均“ CPU”和“经过”毫秒。

理想情况下,您将在Production中运行此程序,以便获得真实的时间估算,但是很少有人允许人们在Production中清除计划缓存。幸运的是,从SQL Server 2008开始,可以从缓存中清除特定计划。在这种情况下,您可以执行以下操作:

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

但是,根据传递给导致“不良”缓存计划的参数的值的可变性,可以考虑另一种方法,该方法介于OPTION(RECOMPILE)和之间OPTION(OPTIMIZE FOR UNKNOWN):动态SQL。是的,我说了。我什至指的是非参数化动态SQL。这就是为什么。

显然,您至少在一个或多个输入参数值方面具有不均匀分布的数据。上述选项的缺点是:

  • OPTION(RECOMPILE)会为每次执行生成一个计划,即使再次传入的参数值与之前的运行相同,您也将永远无法从任何计划重用中受益。对于频繁调用的proc(每隔几秒钟或更频繁地一次),这将使您摆脱偶尔的可怕状况,但仍然使您始终处在一个并非所有的情况下。

  • OPTION(OPTIMIZE FOR (@Param = value)) 会根据该特定值生成计划,这可能会帮助解决多种情况,但仍然使您对当前问题持开放态度。

  • OPTION(OPTIMIZE FOR UNKNOWN)将根据平均分配量生成计划,这将有助于某些查询,但会损害其他查询。这应该与使用局部变量的选项相同。

但是,如果正确完成,Dynamic SQL 将允许传递的各种值具有各自独立的理想查询计划(当然,尽可能多)。此处的主要成本是,随着传入的值的多样性增加,缓存中的执行计划的数量也会增加,并且它们确实会占用内存。较小的费用是:

  • 需要验证字符串参数以防止SQL注入

  • 由于Dynamic SQL需要直接表权限,因此可能需要设置基于证书和基于证书的用户来维护理想的安全性抽象。

因此,当我每秒处理多个过程并击中多个表(每个表具有数百万行)时,这就是我如何处理这种情况的方法。我已经尝试过了,OPTION(RECOMPILE)但是事实证明,在没有参数嗅探/错误的缓存计划问题的99%的情况下,这对过程太有害了。并且请记住,这些proc中的一个包含大约15个查询,并且其中只有3-5个被转换为Dynamic SQL,如此处所述;除非对特定查询必要,否则不使用动态SQL。

  1. 如果存储过程有多个输入参数,请找出哪些参数与数据分布高度不同的列一起使用(从而导致此问题),哪些参数与分布更均匀的列一起使用(并且不应导致此问题)。

  2. 使用与均匀分布的列关联的proc输入参数的参数构建动态SQL字符串。此参数化有助于减少与该查询相关的缓存中执行计划的结果增加。

  3. 对于与高度变化的分布关联的其余参数,应将它们作为文字值连接到Dynamic SQL中。由于唯一查询是由查询文本的任何更改决定的WHERE StatusID = 1,因此,与具有相比,具有不同的查询,因此具有不同的查询计划WHERE StatusID = 2

  4. 如果要连接到查询文本中的任何proc输入参数是字符串,则需要对其进行验证以防止SQL注入(尽管如果传递的字符串是由SQL生成的,则这种情况不太可能发生)应用,而不是用户,但仍然)。至少这样做REPLACE(@Param, '''', '''''')以确保单引号变为转义的单引号。

  5. 如果需要,请创建一个将用于创建用户的证书,并对存储过程进行签名,以便将直接表权限仅授予新的基于证书的用户,而不授予[public]或授予不应该具有此类权限的用户。

示例过程:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;

感谢您抽出宝贵时间回答!我对如何获得编译时间的第一点有些怀疑,因为它比使用@PaulWhite的方法得到的结果低3 。-动态SQL的第二个位很有意思(尽管它也需要一些时间来实现;至少不只是OPTION在查询上打个招呼),而且不会对我造成太大的伤害,因为在集成测试中可以很好地利用此存储过程。-无论如何:感谢您的见解!
Jeroen 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.