如何强制在一个查询中仅对一次标量UDF求值?


12

我有一个查询,需要根据标量UDF的结果进行过滤。该查询必须作为单个语句发送(因此我不能将UDF结果分配给局部变量),并且不能使用TVF。我知道由标量UDF引起的性能问题,其中包括强制整个计划以串行方式运行,过多的内存授予,基数估计问题以及缺少内联。对于这个问题,请假设我需要使用标量UDF。

UDF本身的调用成本非常高,但是从理论上讲,查询可以由优化器以合理的方式实现,使得函数只需要计算一次即可。我为这个问题模拟了一个大大简化的例子。以下查询需要6152毫秒才能在我的计算机上执行:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

查询计划中的过滤器运算符建议针对每行对该函数进行一次评估:

查询计划1

DDL和数据准备:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
WITH
    L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

这是上面示例的db fiddle链接,尽管在此处执行代码大约需要18秒。

在某些情况下,由于供应商提供的功能,我可能无法编辑该功能的代码。在其他情况下,我可以进行更改。如何强制在一个查询中仅对一次标量UDF求值?

Answers:


17

最终,不可能强制SQL Server在查询中仅评估一次标量UDF。但是,可以采取一些步骤来鼓励它。通过测试,我相信您可以获得与当前版本的SQL Server兼容的功能,但是将来的更改可能需要您重新访问代码。

如果有可能编辑代码,那么尝试做的一件好事就是在可能的情况下使函数具有确定性。Paul White在指出,必须使用SCHEMABINDING选项创建函数,并且函数代码本身必须是确定性的。

进行以下更改后:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

该问题的查询在64毫秒内执行:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

查询计划不再具有过滤器运算符:

查询计划1

为确保它仅执行一次,我们可以使用SQL Server 2016中发布的新sys.dm_exec_function_stats DMV:

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');

ALTER针对该函数发出会重置该execution_count对象的。上面的查询返回1,表示该函数仅执行一次。

请注意,仅仅因为函数是确定性的,并不意味着它将仅对任何查询评估一次。实际上,对于某些查询,添加SCHEMABINDING会降低性能。考虑以下查询:

WITH cte (UDF_VALUE) AS
(
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

DISTINCT添加了多余的内容以摆脱Filter运算符。该计划看起来很有希望:

查询计划2

基于此,人们希望对UDF进行一次评估,并将其用作嵌套循环联接中的外部表。但是,该查询需要6446毫秒才能在我的计算机上运行。根据sys.dm_exec_function_stats功能执行了100次。那怎么可能呢?Paul White 在“ 计算标量,表达式和执行计划的性能 ”中指出,可以推迟计算标量运算符:

通常,“计算标量”只是定义一个表达式。实际的计算将推迟到执行计划中的某些后续结果需要为止。

对于此查询,似乎将UDF调用推迟到需要时才进行,此时它被评估了100次。

有趣的是,当未使用定义UDF时,CTE示例在我的计算机上的执行时间为71 ms SCHEMABINDING,就像原始问题一样。运行查询时,该函数仅执行一次。这是该查询计划:

查询计划3

目前尚不清楚为什么不推迟计算标量。可能是因为函数的不确定性限制了查询优化器可以执行的运算符的重新排列。

另一种方法是向CTE添加一个小表,并查询该表中的唯一行。任何小表都可以,但是请使用以下内容:

CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);

INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);

该查询将变为:

WITH cte (UDF_VALUE) AS
(       
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
    FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

增加的dbo.X_ONE_ROW_TABLE添加为优化器增加了不确定性。如果表有零行,则CTE将返回0行。无论如何,如果UDF不是确定性的,优化器就不能保证CTE将返回一行,因此,似乎有可能在联接之前对UDF进行了评估。我希望优化器进行扫描dbo.X_ONE_ROW_TABLE,使用流聚合来获取返回的一行的最大值(这需要对函数进行求值),并将其用dbo.X_100_INTEGERS作主查询中嵌套循环联接的外部表。这似乎是发生了什么

查询计划4

该查询将在我的计算机上执行大约110毫秒,并且根据只会对UDF进行一次评估sys.dm_exec_function_stats。说强制查询优化器只对UDF进行一次评估是不正确的。但是,很难想象优化程序会重写,即使在UDF和计算标量成本方面存在局限性,也会导致查询成本降低。

总之,对于确定性函数(必须包含SCHEMABINDING选项),请尝试以尽可能简单的方式编写查询。如果在SQL Server 2016或更高版本上,请使用确认该功能仅执行一次sys.dm_exec_function_stats。在这方面,执行计划可能会产生误导。

对于SQL Server不认为是确定性的功能,包括缺少SCHEMABINDING选项的任何功能,一种方法是将UDF放在精心制作的CTE或派生表中。这需要一些注意,但是相同的CTE既可以用于确定性功能也可以用于非确定性功能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.