内联表重视 UDF 性能

Posted

技术标签:

【中文标题】内联表重视 UDF 性能【英文标题】:Inline table valued UDF performance 【发布时间】:2011-08-02 20:26:12 【问题描述】:

我使用的是 SQL Server 2008R2。我编写了以下表值 UDF,它接受标量值 this 或 that 或两者都作为参数,并返回包含列 ID、this 和 that 的表。当我从复杂查询中调用此函数时,我看到了糟糕的性能,但在简单查询中调用它时却没有。我想知道是否有人对我正在做的事情有任何想法,这会减慢速度。函数定义如下:

CREATE function dbo.fn_getThisThat (@this nvarchar(255), @that nvarchar(255))
RETURNS TABLE
RETURN

SELECT These.this, Those.that, COALESCE(These.ID, Those.ID) as ID
FROM 
    (
    SELECT col1 as ‘this’, value1, value2, ID
    FROM (
        SELECT t1.col1, t1.col2, t1.col3, t2.col1
        FROM t1
        JOIN t2
            ON t1.col1 = t2.col1
        WHERE t2.col2 = ‘this’
        AND t1.col1 in (‘value1’, ‘value2’)
        ) SOURCE
    PIVOT (
        MAX(t1.col3) FOR t1.col1 in (value1, value2)
        ) AS pvt
    ) These
JOIN
    (
    SELECT t1.col1, t1.col2, t2.col1, t3.ID
    FROM t3
    JOIN t1
        ON t3.col1 = t1.col1
    JOIN t2
        ON t2.col1 = t1.col1
    WHERE t3.col3 = ‘value3’
    AND t1.col3 = ‘value1’
    AND t2.col3 = ‘value2’
    ) Those
WHERE that = @that
OR this = @this

在传递标量参数时,以下语句的处理速度非常快(

SELECT * FROM dbo.fn_getThisThat(scalarValue, null)

或者在一个相对简单的查询中,例如:

SELECT t1.col1, t1.col2, fn.This
FROM t1
CROSS APPLY dbo.fn_getThisThat(t1.col3, null)

...但是当在这样的更复杂的查询中调用时(在伪代码中:如果信息不足,请告诉我),它会滞后(从 ~1 秒到 ~2:30 秒的处理时间):

DECLARE @table (a, b, c)
INSERT @table (a, b, c)
SELECT (values)

SELECT t1.c1, t1.c2, t1.c3
FROM
    (
    SELECT a.c1, COUNT(fn.That) as c2, COUNT(a.c2) as c3
    FROM a
    JOIN b ON (join terms)
    CROSS APPLY dbo.fn_getThisThat(a.c2, null) fn
    WHERE a.c1 IN (SELECT a FROM @table)
    GROUP BY a.c1
    ) t1

有人对我在第二个查询中降低速度的方法有什么建议吗?我将函数更改为接受数组而不是标量参数,但这消除了我交叉应用的能力(在最后一个代码片段中)。据我从查询分析器可以看出,性能损失来自我的函数的交叉应用。我以为我不会遇到 RBAR,因为我的 UDF 不是多语句,但也许我错了……?

编辑: 还有一件事:查询执行计划显示函数本身对批处理的贡献只有 2%;较大的查询贡献了 98%,但其大部分成本来自索引搜索和表扫描,而不是来自并行性。这让我觉得,也许函数调用并不是查询迟缓的原因,而是一些涉及的表缺乏索引(不幸的是,我对添加指数。)。我在没有调用函数的情况下运行了查询,表扫描和索引搜索仍然显示很高,但查询在大约 8 秒内完成。那么,我们回到函数...?

【问题讨论】:

【参考方案1】:

您可能希望更改您的 UDF 以在任何地方正确使用由两部分组成的表名,以便您可以向其中添加 SCHEMABINDING 子句。见Improving query plans with the SCHEMABINDING option on T-SQL UDFs。

【讨论】:

【参考方案2】:

来自 MSDN 文章 Apply (MSDN - Apply):

“APPLY 运算符允许您为查询的外部表表达式返回的每一行调用表值函数。”

您的示例显示了分组依据。是否可以在对行进行分组后而不是在该特定查询中调用您的函数?这将减少必须调用该函数的行数。

如果做不到这一点,我的另一个建议是通过优化那里的查询来尽可能地提高函数本身的性能。你可以使每毫秒快一点,它就会加起来。

【讨论】:

感谢您的建议:我不确定我是否可以在应用该功能之前进行分组,因为它可以作为“翻译器”将其转换为那个,反之亦然。想法是计算这个并按 ID 分组:据我所知,如果我先分组,我会尝试将整数转换为这个或那个,这是行不通的。【参考方案3】:

正如已经指出的,对于外部查询中的每一行都会调用 CROSS APPLY。所以,这里的关键问题是从多少行返回:

DECLARE @table (a, b, c)
INSERT @table (a, b, c)
SELECT (values)

SELECT t1.c1, t1.c2, t1.c3
FROM
    (
    SELECT a.c1
    FROM a
    JOIN b ON (join terms)
    WHERE a.c1 IN (SELECT a FROM @table)
    ) t1

这是向您的 TVF 发出的调用次数。如果(这是一个很大的假设)对于任何 a.c2 值,TVF 具有相似的执行时间,那么相关的性能比较是您的函数的奇异执行时间 * 从上面的查询返回的行。

由于原始查询的混淆/概括,很难确定,但我怀疑您的 TVF 可能会被消除,并且逻辑内联到父查询。如果可行,这可能会为您带来最佳性能。

【讨论】:

谢谢,马克!你对我的概括是对的:我担心我把它归结得太远了。您上面引用的伪代码选择语句是 4 个之一,每个都调用该函数。如果我要消除该函数并在线应用逻辑,我将不得不在此查询中执行 4 次(并且在整个系统中执行更多次)。我看到我正在为交叉应用的每一行调用该函数,但考虑到它是单语句,因此认为它相对便宜。【参考方案4】:

我认为最好的办法是在 SSMS 中运行它并检查你的执行计划。由于这是一个内联表值 UDF,因此优化器会将其合并到执行计划中,您应该能够看到哪里出了问题。

我没有太多在 CROSS APPLY 情况下使用 PIVOT 子查询的经验——这让我觉得这可能是个问题。但执行计划肯定会告诉你。

【讨论】:

【参考方案5】:

到目前为止,我已经能够将性能从 ~2:30 提高到 ~0:17。它更好,但仍然不理想。我做了以下事情:

为我的电视 udf 添加了模式绑定(谢谢你,Remus!)。这有帮助,但对性能的影响似乎比以下情况要小。

重组主查询以加入 @table 而不是在子查询中引用它:这似乎最有帮助,而且似乎大部分性能提升都来自于此。

我认为我剩下的滞后是由于我重击的大表上缺少一些索引,但没有添加它们的能力,我不确定我能做什么 atm。根据查询分析器的报告,我已将并行性的成本降低到 0%,因此我认为在函数调用方面我已尽我所能。

谢谢大家!

【讨论】:

小心,QA/SSMS 中的执行计划百分比是估计的,而不是实际成本。特别是并行性会严重扭曲计划中的估计与实际成本。此外,作为您的新手……您认为有用的任何答案都应该被“投票”,您认为可以解决问题的任何答案都应该被标记为这样。

以上是关于内联表重视 UDF 性能的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server StoredProc 与 UDF 内联表

如何创建一个 CROSS APPLYs 一系列内联表值 UDF 的视图?

如何在 SQL Server 中将拆分函数转换为内联表值 udf?

执行存储过程内联,类似于 UDF?

ORACLE 在线表重定义

使用复制表重命名数据库