未存储标量函数值

use*_*949 5 sql-server optimization functions

我创建了一个标量值函数,它在传入的秒数中暂停。这个函数不会在生产中使用。如果我向查询添加 UDF,我正在尝试测试 SQL Server 如何评估和创建计划。我对它如何评估我的 3 个查询中的最后一个的差异感到非常困惑。首先是创建测试函数的代码:

CREATE FUNCTION dbo.[sleep](@seconds int)
RETURNS datetime
as
BEGIN

DECLARE @sleepUntil datetime
DECLARE @dummy int
SET @sleepUntil = DATEADD(s, @seconds, getdate())
WHILE getdate() < @sleepUntil
    SET @dummy = 0
RETURN getdate()
END
Run Code Online (Sandbox Code Playgroud)

现在创建一个表来测试查询

CREATE TABLE #table (num int)
Run Code Online (Sandbox Code Playgroud)

并插入值

INSERT INTO #table 
VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)
Run Code Online (Sandbox Code Playgroud)

第一个查询需要 2 秒才能运行 - 假设 SQL Server 存储从标量函数返回的值,这是有道理的。

   SELECT num
   FROM #table t
   WHERE dbo.sleep(2)=0 
Run Code Online (Sandbox Code Playgroud)

第二个仍然需要 2 秒 - 所以我的假设是它也一样。

 SELECT num
   FROM #table t
   WHERE EXISTS (select TOP 1 1 
                FROM  #table t1 ) 
   OR dbo.sleep(2)=0 
Run Code Online (Sandbox Code Playgroud)

第三个令人费解 - 运行需要 20 秒,为什么 sql server 不将值存储在这里而是为每一行运行它?我注意到的一个有趣的点是,当我单击查看实时查询统计信息时,开始显示计划需要 20 秒。

SELECT num
   FROM #table t
   WHERE EXISTS (select TOP 1 1 
                FROM  #table t1 
                WHERE t.num = t1.num) dbo.sleep(2)=0 
Run Code Online (Sandbox Code Playgroud)

Joe*_*ish 10

要回答这样的问题,您需要仔细检查计划。我在这里问并回答了一个类似的问题。它可能值得一读,但最重要的部分是可以推迟计算标量,并且 SQL Server 2016 为我们提供了sys.dm_exec_function_stats DMV,以便更容易查看 UDF 在查询中执行的次数。

您的示例 UDF 是不确定的,它可以更改它在查询中执行的次数。的使用getdate()和缺乏SCHEMABINDING使其具有不确定性。您也可以在 SQL Server 中验证这一点:

SELECT OBJECTPROPERTY(OBJECT_ID('dbo.[sleep]'), 'IsDeterministic')
Run Code Online (Sandbox Code Playgroud)

让我们来看看您的查询。对于第一个查询,UDF 调用位于启动表达式谓词中:

第一次查询启动

过滤操作符在 9 行上执行一次。启动表达式谓词意味着对于过滤器处理的每一行,UDF 只执行一次而不是一次。这就是为什么这个查询只需要 2 秒。

如果我将查询更改为以下内容:

SELECT num
FROM #table t
WHERE dbo.sleep(2)=DATEADD(DAY, num, 0);
Run Code Online (Sandbox Code Playgroud)

现在查询需要 18 秒。在计划中我们可以看到过滤器不再是启动表达式谓词:

查询 1 更改

现在每行执行一次 UDF。如果我创建一个全新的确定性函数:

CREATE FUNCTION dbo.[no_sleep] (@seconds int)
RETURNS datetime
WITH SCHEMABINDING
as
BEGIN
    RETURN DATEADD(DAY, 0, 0);
END;
Run Code Online (Sandbox Code Playgroud)

现在查询计划再次更改:

查询计划阻止

对于此查询,UDF 仅执行一次。您可以通过 UDF、使用扩展事件或使函数执行花费很长时间(但它需要保持确定性)来验证这一点。

对于问题中的第二个查询:

SELECT num
   FROM #table t
   WHERE EXISTS (select TOP 1 1 
                FROM  #table t1 ) 
   OR dbo.sleep(2)=0;
Run Code Online (Sandbox Code Playgroud)

出于不同的原因,每个查询只执行 UDF。WHERE子句的结果不引用表,t并且 SQL Server 选择有效地将它们缓存到一个假脱机中:

第二次查询

如果您查看实际计划,您会看到 spool 执行了 9 次,但过滤器仅在一行中执行了一次。

您的第三个查询无法编译,但我认为您的意思是:

SELECT num
   FROM #table t
   WHERE EXISTS (select TOP 1 1 
                FROM  #table t1 
                WHERE t.num = t1.num) OR dbo.sleep(2)=0; 
Run Code Online (Sandbox Code Playgroud)

现在,你从引用列tWHERE条款不再有在计划阀芯:

第三次查询

UDF 引用是一个启动表达式过滤器,但它仍会执行 9 次,每次执行过滤器时执行一次。如果我将查询更改为使用确定性no_sleep函数,则 UDF 只执行一次,尽管查询计划根本没有改变。

总而言之,根据函数定义和查询定义,函数可以每个查询执行一次或每行执行一次。有时,仅通过查看计划就很难确定 UDF 将执行多少次。如果您需要保证一个函数只执行一次,您应该将值保存到局部变量并在查询中使用该局部变量,并在RECOMPILE需要参数嵌入优化时提供提示。