我正在尝试重构一些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]
这的确比重构之前执行得慢一些;但是降级是最小的,出于维护性和可读性的考虑,我认为值得。
答案 0 :(得分:2)
从历史上看,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 );
之所以可行,是因为一切都来自一张桌子。