hoy*_*tdj 2 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]
Run Code Online (Sandbox Code Playgroud)
重构之前,查询运行大约需要 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]
Run Code Online (Sandbox Code Playgroud)
这确实比重构之前执行得慢一些;但降级是最小的,我认为,为了可维护性和可读性,这是值得的。
T-SQL 中的标量函数历来对查询性能不利有几个原因(尽管这有望很快得到改善 - 我将在最后解释)。
一般来说,过去 10 多年的大多数 SQL Server 指南都建议不要使用标量 T-SQL 函数,原因我已经解释过。您会发现的大多数外部内容可能都符合这个概念。请注意,历史上 SQL Server确实内联单语句 T-SQL 表值函数(将它们视为 SQL 中的视图),但这是一个完整的历史工件,它显然与 T-SQL 标量函数处理不一致。
Microsoft 的 QP 团队了解这些已经有一段时间了。然而,修复这些问题需要大量工作才能使系统达到标量 T-SQL 函数内联通常可以帮助所有客户并且不会导致某些查询变慢的形式。不幸的是,大多数商业优化器的工作方式创建了一个模型,该模型根据计算工作方式的某些假设来估计运行时间。该模型将是不完整的(例如:正如我所指出的,我们今天根本不花费 t-sql 标量函数)。拥有模型的一个不明显的副作用是,某些查询将在模型之外(这意味着优化器正在猜测或使用不完整的数据)并得到一个很好的计划。有些查询将在模型之外并得到一个糟糕的计划。模型内的查询并不总是能得到很好的计划,但平均而言它们会做得更好。更进一步,如果成本或一组考虑的替代方案从 SQL 的一个主要版本更改为下一个版本,那么当您升级时,您可能会开始获得与以前不同的计划。对于那些“模型之外”的情况,效果是相当随机的 - 在某些情况下您可以获得更快或更慢的计划。因此,如果没有一套机制来找到防止计划回归的机制,则更改优化器的成本模型变得非常困难 - 否则许多客户将有一些针对一组不完整假设进行“调整”的查询,然后变得更糟计划何时发生这些变化。Net-net:优化器团队没有改变成本模型来解决这个问题,因为平均而言,这会比处理痛苦造成更多的客户伤害,直到有足够的机制在升级时提供良好的客户体验。
在过去的几个版本中,这正是 SQL 团队一直在做的事情。首先,对成本模型或所考虑的计划集(称为搜索空间)的任何更改都更改为与数据库的兼容性级别相关联。这允许客户升级并保持旧的兼容性级别,因此通常不会在同一硬件上看到计划更改。它还允许客户尝试更换到新的,如果工作负载出现问题,则立即停止,从而大大降低了以前单向升级的风险。您可以在此处阅读有关升级建议的更多信息。其次,SQL 团队添加了一个“飞行数据记录器”,用于随着时间的推移选择计划,称为查询存储。它捕获先前的计划和这些计划的执行情况。这允许客户“返回”之前的计划(如果速度更快)(即使您受到其中一种不适用模型案例的影响)。这提供了另一个级别的保险,防止升级时破坏应用程序。
(抱歉,这很冗长 - 上下文很重要)。
对于 SQL Server 2019 + SQL Azure,QP 团队引入了一种内联许多 T-SQL 标量函数的机制。您可以在此处阅读该公告。仍然有一些启发式方法正在对此功能进行调整,以确保与不内联相比,几乎没有/没有性能回归(这意味着 QP 通常会弄清楚什么时候内联与不内联更好,并且只内联这些情况)。内联时,优化器能够对连接重新排序并考虑各种计划选择替代方案,以获得更快的查询计划。因此,最终,这会在查询处理器内使用正常的关系运算符,并以这种方式消耗它们。
我希望这能解释为什么现在对您来说速度可能会变慢,并给您带来一些希望,即正如我们所说,SQL 团队确实正在努力改进这种情况。祝您调整应用程序好运。