【问题标题】:Checking variable for NULL kills performance检查 NULL 变量会降低性能
【发布时间】:2017-09-06 07:29:43
【问题描述】:

我有以下疑问:

DECLARE @application_number CHAR(8)= '37832904';
SELECT
    la.LEASE_NUMBER AS lease_number,
    la.[LEASE_APPLICATION] AS application_number,
    tnu.[FOLLOWUP_CODE] AS note_type_code -- catch codes not in codes table
FROM [dbo].[lease_applications] la
LEFT JOIN [dbo].tickler_notes_uniq tnu ON tnu.[ACCOUNT_NUMBER] = la.[ACCOUNT_NUMBER]
WHERE la.LEASE_APPLICATION = @application_number
      OR @application_number IS NULL;



SELECT
    la.LEASE_NUMBER AS lease_number,
    la.[LEASE_APPLICATION] AS application_number,
    tnu.[FOLLOWUP_CODE] AS note_type_code -- catch codes not in codes table
FROM [dbo].[lease_applications] la
LEFT JOIN [dbo].tickler_notes_uniq tnu ON tnu.[ACCOUNT_NUMBER] = la.[ACCOUNT_NUMBER]
WHERE la.LEASE_APPLICATION = @application_number; 

这两个查询之间的唯一区别是我添加了检查变量是否为 NULL。

这些查询的执行计划是:

您可以找到图形计划here

所以问题是。为什么计划如此不同?

更新:

第一个查询的实际执行计划可以找到here

OPTION(RECOMPILE) 将实际执行计划更改为好的计划。然而,这样做的缺点是我的主要目标是使用这些参数创建 TVF,然后使用该功能的每个人都应该提供该选项。

值得一提的是,我的主要目标是创建具有 2 个参数的 TVF。每一个都可能为空,也可能不是,但至少有一个应该是 NOT NULL。这些参数或多或少相等,它们只是两个表中的不同键,无论如何都会给出相同的结果(相同的行数等等)。这就是为什么我想做类似的事情

WHERE (col1 = @param1 OR @param1 IS NULL) AND (col2 = @param2 OR @param2 IS NULL) AND (@param1 IS NOT NULL or @param2 IS NOT NULL)

所以,基本上我对所有记录都不感兴趣

【问题讨论】:

  • 因为第一个考虑到@application_number NULL 的可能性,这意味着条件la.LEASE_APPLICATION = @application_number 可能根本不会过滤任何行。第二个知道。
  • 在这种情况下它不为空,但优化器必须生成一个对所有可能情况都有效的计划。尝试添加WITH (RECOMPILE),这至少应该使估计值朝正确的方向倾斜(但可能不会改变实际计划)。 OPTION (OPTIMIZE FOR (@application_number = '37832904')) 是另一种可能性。同样,当缓存的计划被重用时,实际计划仍然必须考虑到运行时该值为 null 的可能性。 OR @x IS NULL 条件很麻烦,这就是为什么人们经常使用IF 并拆分语句。
  • OPTION (RECOMPILE) 不是WITH (RECOMPILE)。然后 SQL Server 将在每次调用时重新编译语句而不缓存计划 - 因此它可以针对特定值进行优化。但是你每次都要支付编译费用。
  • 而且你不会在估计的计划中看到OPTION (RECOMPILE)的效果。您将需要实际执行查询并获取实际计划。

标签: sql-server sql-server-2014 sql-execution-plan sqlperformance


【解决方案1】:

对于两个不同的查询,您有两个不同的计划。 有意义的是,当您在 WHERE 子句(la.LEASE_APPLICATION = @application_number)(并具有适当的索引)上有一个相等条件时,您会得到一个 index seek:按预期工作!

另一方面,当您将这两个条件写入一个WHERE 子句(la.LEASE_APPLICATION = @application_number OR @application_number IS NULL) 时,查询优化器已选择进行扫描。

即使提供了参数值并且它不为空,正在使用的计划是缓存的计划,它在编译时无法知道参数的实际值。

****如果您有一个存储过程并且使用参数调用它,就会出现这种情况。使用变量执行简单查询时并非如此。 正如@sepupic 所说,变量值不会被嗅探。****

生成计划以处理两种情况:当您的参数有值时以及当您没有参数时。

解决您的问题的一个选项是使用OPTION(RECOMPILE),因为它已在 cmets 中说明。 另一种选择是将您的查询分开(例如,有两个不同的存储过程,由第三个“包装”过程调用),以便它们得到相应的优化,每个都是自己的。

我建议您看看 Kimberly L. Tripp 的这篇文章:Building High Performance Stored Procedures 和 Aaron Bertrand 的另一篇文章:An Updated "Kitchen Sink" Example。我认为这些是解释这种情况的最佳文章。

两篇文章都解释了这种情况,可能存在的问题)以及可能的解决方案,例如选项(重新编译)、动态 sql 或具有分离的存储过程。

祝你好运!

【讨论】:

  • 这里的问题是我要创建TVF。请阅读我的问题中的更新部分。我对扫描完全不感兴趣,因为我总是使用密钥来获取记录(来自 param1 或 param2)
  • @DmitrijKultasev 您可以在语句级别指定选项(重新编译)。您可以在您的函数中已经定义它,并且使用您的函数的人不需要编写选项(重新编译)。
  • >>>即使提供了参数值并且它不为空,正在使用的计划是缓存的计划,它在编译时无法知道参数的实际值。
  • @Rigerta Demiri 当然,您的信息对模块很有用,因为在这种情况下,计划是根据第一个嗅探的参数值构建的,并且无需重新编译,即使其他参数也将使用缓存的计划跨度>
  • @DmitrijKultasev TVF 不是存储过程。调用时它会被其实际文本替换。这意味着您不会从创建厨房水槽 TVF 中获得任何好处。只是不要这样做。编写一个视图来隐藏查询的复杂性并每次都使用适当的过滤器。像 EF、Dapper 这样的 ORM 让这变得微不足道
【解决方案2】:

您的查询不使用parameters,而是使用variable。在编译批处理时不会嗅探变量(编译=制定计划),因为批处理被视为一个整体。所以服务器不知道变量是空还是不空。它必须制定一个适合这两种情况的计划。

第一个查询根本无法过滤任何行,因此选择了扫描。

第二个查询确实过滤了,但是值未知,所以如果你使用SQL server 2014并且fintered列不是唯一的,估计是C^3/4(C=表基数)

如果您使用RECOMPILE 查询选项,情况可能会有所不同。当您将它添加到查询中时,它会在表变量的分配完成后重新编译。在这种情况下,变量值是已知的,您将获得另一个计划。这将是一个基于过滤器已知值的列统计信息的计划

【讨论】:

  • 变量的值是否被嗅探在这里无关紧要。问题是 SQL Server 必须缓存一个对传递的任何参数/变量值都有效的计划。如果 OP 要使用参数(被嗅探),他们仍然会遇到同样的问题,即添加 OR @application_number IS NULL 会导致更糟糕的计划。
  • @Martin Smith 你是对的。我只是试图解释使用 RECOMPILE 选项时会发生什么变化。以它的形式编写的第一个查询将在参数/变量的情况下导致相同的计划,只是因为它不知道它是否为空,所以该计划也必须对空有效
猜你喜欢
  • 2014-12-25
  • 2021-06-07
  • 2016-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-01
  • 2018-12-12
相关资源
最近更新 更多