为什么SELECT语句中的简单函数调用会大大降低我的查询速度?

时间:2019-01-21 22:52:56

标签: sql sql-server query-performance

我正在尝试重构一些SQL代码以使其更具可读性和可维护性。但是,我不想破坏性能。我正在尝试将select语句中的某些列逻辑移至几个函数中,但是看到性能大幅下降。我希望大家能帮助我理解原因;甚至更好的解决方法!

重构后,我的代码大致类似于下面的示例。 在重构之前,而不是在函数调用之前, CASE语句直接位于select子句中的SUM函数之内

FUNCTION funcOne(@colA, @colB, @colC, @valX, @valY)
RETURNS INT AS
BEGIN
    RETURN CASE
        WHEN @colA = @colB
            THEN @valX + @valY
        WHEN @colC BETWEEN 1 AND 10
            THEN @valX
        ELSE 0
    END
END

FUNCTION funcTwo(@colA, @colB, @colC, @valX, @valY)
RETURNS INT AS
BEGIN
    RETURN CASE
        WHEN @colA <> @colB
            THEN @valX + @valY
        WHEN @colC BETWEEN 1 AND 10
            THEN @valY
        ELSE 0
    END
END

SELECT mt.[Ident]
    ,SUM(funcOne(mt.[colA], ot.[colB], ot.[colC], mt.[valX], ot.[valY])) AS funcOne
    ,SUM(funcTwo(mt.[colA], ot.[colB], ot.[colC], mt.[valX], ot.[valY])) AS funcTwo
FROM MyTable AS mt
INNER JOIN SomeOtherTable AS ot
    ON mt.[Ident] = ot.[Ident]
WHERE mt.[colA] BETWEEN 1 AND 100
GROUP BY mt.[Ident]

在重构查询之前,运行查询大约需要60秒钟。重构后将花费近7分钟!扫描次数和读取次数是相同的,所以对我来说奇怪的是它花费了这么长时间。

SQL做什么使重构后的效率如此低下?无论如何,有什么办法可以解决这个问题,并保持我可读的代码?

解决方案

感谢所有的“为什么?”信息,@ conor-cunningham-msft。

就解决性能问题而言,我最终使用了@Simonare等人的建议。

这是我的代码:

FUNCTION funcOne(@colA, @colB, @colC, @valX, @valY)
RETURNS TABLE AS
RETURN (
    SELECT CASE
        WHEN @colA = @colB
            THEN @valX + @valY
        WHEN @colC BETWEEN 1 AND 10
            THEN @valX
        ELSE 0
    END AS [MyValue]
)

FUNCTION funcTwo(@colA, @colB, @colC, @valX, @valY)
RETURNS TABLE AS
RETURN (
    SELECT CASE
        WHEN @colA <> @colB
            THEN @valX + @valY
        WHEN @colC BETWEEN 1 AND 10
            THEN @valY
        ELSE 0
    END AS [MyValue]
)

SELECT mt.[Ident]
    ,SUM(funcOne.[MyValue]) AS funcOneValue
    ,SUM(funcTwo.[MyValue]) AS funcTwoValue
FROM MyTable AS mt
INNER JOIN SomeOtherTable AS ot
    ON mt.[Ident] = ot.[Ident]
CROSS APPLY funcOne(mt.[colA], ot.[colB], ot.[colC], mt.[valX], ot.[valY]) AS funcOne
CROSS APPLY funcTwo(mt.[colA], ot.[colB], ot.[colC], mt.[valX], ot.[valY]) AS funcTwo
WHERE mt.[colA] BETWEEN 1 AND 100
GROUP BY mt.[Ident]

这的确比重构之前执行得慢一些;但是降级是最小的,出于维护性和可读性的考虑,我认为值得。

3 个答案:

答案 0 :(得分:2)

从历史上看,T-SQL中的标量函数不利于查询性能有两个原因(尽管有望很快得到改善-我将在最后解释)。

  • 首先,即使T-SQL标量函数仅包含标量逻辑(没有查询或联接),在查询中间调用解释函数也有开销。对于具有许多行且T-SQL标量处理速度较慢的查询,此开销是可以测量的。我不确定在任何情况下都会使您的查询速度降低数倍,但是还有其他原因可能会降低速度。
  • 第二,查询优化器中处理标量运算的方式可能会对您的性能产​​生不良影响,而这是您所不希望的。在SQL查询优化开始时使用的一种启发式方法是将标量运算向下推到查询的末尾。这旨在允许优化器匹配计算列(可以持久保存,从而可以加快昂贵的标量计算)。但是,这样做的负面影响是,非持久性计算的执行频率可能比您预期的要高。因此,如果您的过滤器/联接具有从查询输出的1000行,但处理了1MM的行以产生该结果,则标量函数可能执行1MM次,而不是您在查询中编写的1000次。从历史上看,优化器假定标量函数的成本为零,并且没有试图推断它们的执行成本(如果您有兴趣了解更多有关此知识的信息,请参考一些历史记录)。
  • 第三,如果碰巧将子查询或“查找”放入t-sql标量函数中,则可以实现代码分解,但是通过阻止优化器看到可能加快查询速度的连接顺序,完全蒙住了优化器。因此,尽管在程序语言中尝试像这样的通用编码模式完全有意义,但是当优化器需要重写所有内容以获取尽可能最快的查询计划时,它却毫无意义。

通常,由于我已解释的原因,过去10多年中有关SQL Server的大多数指南建议不要使用标量T-SQL函数。您将发现的大多数外部内容都可能与此概念保持一致。请注意,从历史上看,SQL Server did 内联单语句T-SQL表值函数(将它们当作视图在SQL中进行处理),但这是一个完整的历史工件,显然与T-SQL标量函数处理。

Microsoft的QP团队已经对此有所了解。但是,修复这些问题需要大量工作才能将系统转换为标量T-SQL函数内联通常可以帮助所有客户的形式,而不会导致某些查询变慢。不幸的是,大多数商业优化器的工作方式都创建了一个模型,该模型根据一些计算工作原理的假设来估计运行时间。该模型将是不完整的(例如:如我所述,我们今天根本不花费t-sql标量函数)。拥有模型的一个显而易见的副作用是,某些查询将不在模型之外(这意味着优化器正在猜测或使用不完整的数据)并获得了一个不错的计划。一些查询将超出模型并获得可怕的计划。模型中的查询并非总能得到很好的计划,但平均而言它们会做得更好。更进一步,如果成本或考虑的替代方案集从一个主要的SQL版本更改为下一个主要版本,则在升级时,您可能将开始获得与以前不同的计划。对于那些“模型之外”的情况,效果是非常随机的-在某些情况下,您可以获得更快或更慢的计划。这样,如果没有一套机制来防止预防计划的回归,就很难更改优化器的成本模型-否则,许多客户将针对一组不完整的假设对“查询”进行“调整”,然后变得更糟。计划何时从他们下面改变。 Net-net:优化人员团队没有去改变成本模型来解决此问题,因为在解决问题之前,平均而言,它会给客户造成更大的伤害,而在没有足够的机制来升级时提供良好的客户体验。

在过去的几个发行版中,这正是SQL团队一直在做的事情。首先,对成本模型或所考虑的计划集的任何更改(称为搜索空间)都已更改为与数据库的兼容性级别相关联。这允许客户升级,保留旧的兼容性级别,因此通常不会在同一硬件上看到计划更改。它还允许客户尝试更改为新版本,并在工作负载出现问题时立即停机,从而大大降低了以前单向升级的风险。您可以阅读有关升级建议here的更多信息。其次,SQL团队添加了一个“飞行数据记录器”,用于随时间推移进行计划选择,称为查询存储。它捕获先前的计划和这些计划的执行情况。如果速度更快(即使您受到其中一种超出模型的案例的影响),这也可使客户“回到”先前的计划。这为防止升级时破坏应用程序提供了另一种保障。

(很抱歉,这很冗长-上下文很重要)。

对于SQL Server 2019 + SQL Azure,QP团队引入了一种内联许多T-SQL标量函数的机制。您可以阅读公告here。仍然需要对该功能进行启发式调整,以确保与不内联相比,性能退化很少/没有(这意味着QP通常会指出内联与不内联比较好,仅内联那些情况下更好)。内联时,优化器可以重新排序联接并考虑各种计划选择备选方案,从而为您提供更快的查询计划。因此,最后,这将在查询处理器内部使用常规的关系运算符,并以此为代价。

我希望能解释一下为什么现在情况可能会变慢,并且希望您能像我们所说的那样,SQL团队确实正在努力改善这种情况。祝您调试应用程序好运。

答案 1 :(得分:1)

就性能而言,标量值函数通常是不好的做法。假设您有功能

CREATE FUNCTION fn_GetName(@CustomerID int)
RETURNS varchar(100)
AS 
RETURN (
  DECLARE @myResult VARCHAR(100);
  SELECT @myResult =  Fullname
  FROM Sales s
  WHERE s.CustomerID = @CustomerID
  RETURN @myResult 
 )

并说我们像这样调用该函数

select 
   fn_GetName(id)
from Student;

SQL正在逐行解释此函数,这会导致性能瓶颈。但是,表值函数不执行逐行操作

CREATE FUNCTION fn_GetNameTable(@CustomerID int)
RETURNS TABLE
AS 
RETURN (  
  SELECT Fullname
  FROM Sales s
  WHERE s.CustomerID = @CustomerID
 )

然后

SELECT I.[Customer Name]
  ,S.CustomerType
FROM Sales s
CROSS APPLY fn_GetNameTable(S.CustomerID) I

是SQL本机。

您可以从This addess

阅读更多内容

答案 2 :(得分:0)

SQL不能很好地利用函数分解事物。调用用户定义的函数有很多开销。内联函数可能会带来一些改进,但我建议您使用计算列:

alter table mytable add funcone as ( your `case` expression here );

之所以可行,是因为一切都来自一张桌子。