SQL Server 是否缓存多语句表值函数的结果?

Pau*_*ite 24 sql-server execution-plan set-returning-functions

多语句表值函数在表变量中返回其结果。

这些结果是否曾被重用,还是每次调用时总是对函数进行全面评估?

Pau*_*ite 26

多语句表值函数 (msTVF) 的结果永远不会跨语句(或连接)缓存或重用,但有几种方法可以同一语句中重用 msTVF 结果。就此而言,msTVF 不一定在每次调用时都重新填充。

示例 msTVF

这个(故意低效的)msTVF 返回指定范围的整数,每行都有一个时间戳:

IF OBJECT_ID(N'dbo.IntegerRange', 'TF') IS NOT NULL
    DROP FUNCTION dbo.IntegerRange;
GO
CREATE FUNCTION dbo.IntegerRange (@From integer, @To integer)
RETURNS @T table 
(
    n integer PRIMARY KEY, 
    ts datetime DEFAULT CURRENT_TIMESTAMP
)
WITH SCHEMABINDING
AS
BEGIN
    WHILE @From <= @To
    BEGIN
        INSERT @T (n)
        VALUES (@From);

        SET @From = @From + 1;
    END;
    RETURN;
END;
Run Code Online (Sandbox Code Playgroud)

静态表变量

如果函数调用的所有参数都是常量(或运行时常量),则执行计划将填充表变量结果一次。计划的其余部分可能会多次访问表变量。可以从执行计划中识别表变量的静态性质。例如:

SELECT
    IR.n,
    IR.ts 
FROM dbo.IntegerRange(1, 5) AS IR
ORDER BY
    IR.n;
Run Code Online (Sandbox Code Playgroud)

返回类似于以下内容的结果:

简单的结果

执行计划是:

简单的执行计划

Sequence 运算符首先调用 Table Valued Function 运算符,该运算符填充表变量(注意该运算符不返回任何行)。接下来,序列调用它的第二个输入,它返回表变量的内容(在这种情况下使用聚集索引扫描)。

计划使用“静态”表变量结果的赠品是序列下方的表值函数运算符 - 在计划的其余部分可以开始之前,表变量需要完全填充一次。

多次访问

为了显示被多次访问的表变量结果,我们将使用第二个表,其行编号为 1 到 5:

IF OBJECT_ID(N'dbo.T', 'U') IS NOT NULL
    DROP TABLE dbo.T;

CREATE TABLE dbo.T (i integer NOT NULL);

INSERT dbo.T (i) 
VALUES (1), (2), (3), (4), (5);
Run Code Online (Sandbox Code Playgroud)

以及将这个表连接到我们的函数的新查询(这同样可以写成APPLY):

SELECT T.i,
       IR.n,
       IR.ts
FROM dbo.T AS T
JOIN dbo.IntegerRange(1, 5) AS IR
    ON IR.n = T.i;
Run Code Online (Sandbox Code Playgroud)

结果是:

加入结果

执行计划:

加盟计划

和以前一样,序列首先填充表变量 msTVF 结果。接下来,使用嵌套循环将表T中的每一行连接到 msTVF 结果中的一行。由于函数定义包括对表变量有用的索引,因此可以使用索引查找。

关键在于,当 msTVF 的参数是常量(包括变量和参数)或被执行引擎视为语句的运行时常量时,该计划将为 msTVF 表变量结果提供两个单独的运算符:一个用于填充桌子; 另一个访问结果,可能多次访问表,并可能使用函数定义中声明的索引。

相关参数和非常量参数

为了突出使用相关参数(外部引用)或非常量函数参数时的差异,我们将更改表的内容,T使函数有更多的工作要做:

TRUNCATE TABLE dbo.T;

INSERT dbo.T (i) 
VALUES (50001), (50002), (50003), (50004), (50005);
Run Code Online (Sandbox Code Playgroud)

以下修改后的查询现在T在函数参数之一中使用对表的外部引用:

SELECT T.i,
       IR.n,
       IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Run Code Online (Sandbox Code Playgroud)

此查询大约需要8 秒才能返回如下结果:

相关结果

注意 column 中行之间的时间差ts。该WHERE子句限制了大小合理的输出的最终结果,但效率低下的函数仍然需要一段时间才能用 50,000 多行(取决于i表的相关值T)填充表变量。

执行计划是:

相关执行计划

请注意缺少 Sequence 运算符。现在,有一个表值函数运算符填充表变量并在嵌套循环连接的每次迭代中返回其行。

需要明确的是:表 T 中只有 5 行,表值函数运算符运行了 5 次。它在第一次迭代时生成 50,001 行,第二次生成 50,002 行……依此类推。表变量在迭代之间被“丢弃”(截断),因此五个调用中的每一个都是完整的总体。这就是为什么它如此缓慢,并且每一行出现在结果中的时间大约相同。

旁注:

当然,上面的场景是故意设计来显示当 msTVF 在每次迭代中填充多行时性能有多差。

明智实现上面的代码将设置两个msTVF参数i,并去除多余的WHERE子句。表变量仍然会在每次迭代时被截断和重新填充,但每次只有一行。

我们还可以在前面的步骤中i从中获取最小值和最大值T并将它们存储在变量中。使用变量而不是相关参数调用函数将允许使用“静态”表变量模式,如前所述。

缓存未更改的相关参数

再次回到最初的问题,在无法使用 Sequence 静态模式的情况下,如果自嵌套循环连接的先前迭代以来没有任何相关参数发生更改,则 SQL Server可以避免截断和重新填充 msTVF 表变量。

为了证明这一点,我们将T用五个相同的 i值替换 的内容:

TRUNCATE TABLE dbo.T;

INSERT dbo.T (i) 
VALUES (50005), (50005), (50005), (50005), (50005);
Run Code Online (Sandbox Code Playgroud)

再次带有相关参数的查询:

SELECT T.i,
       IR.n,
       IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Run Code Online (Sandbox Code Playgroud)

这次结果出现在大约1.5 秒内

相同的行结果

请注意每行上的相同时间戳。表变量中的缓存结果可用于后续迭代,其中相关值i不变。重用结果比每次插入 50,005 行要快得多。

执行计划看起来与之前非常相似:

规划相同的行

关键区别是在实际重新绑定实际倒带表值函数算子性质:

运算符属性

当相关参数不变时,SQL Server 可以重放(倒带)表变量中的当前结果。当相关性发生变化时,SQL Server 必须截断并重新填充表变量(重新绑定)。重新绑定发生在第一次迭代;由于 的值T.i不变,因此随后的四次迭代都是倒带的。