多语句表值函数将其结果返回到表变量中。
这些结果是否曾经被重用过,或者每次调用该函数时总是对其进行全面评估?
多语句表值函数将其结果返回到表变量中。
这些结果是否曾经被重用过,或者每次调用该函数时总是对其进行全面评估?
Answers:
多语句表值函数(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;
如果函数调用的所有参数都是常量(或运行时常量),则执行计划将表变量结果填充一次。计划的其余部分可以多次访问表变量。表变量的静态性质可以从执行计划中识别出来。例如:
SELECT
IR.n,
IR.ts
FROM dbo.IntegerRange(1, 5) AS IR
ORDER BY
IR.n;
返回类似于以下内容的结果:
执行计划是:
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);
还有一个新查询,该查询将该表连接到我们的函数中(可以等同地写成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;
结果是:
执行计划:
和以前一样,Sequence首先填充表变量msTVF结果。接下来,使用嵌套循环将表T
中的每一行连接到msTVF结果中的一行。由于函数定义在表变量上包括有用的索引,因此可以使用索引查找。
关键点是,当msTVF的参数是常量(包括变量和参数)或被执行引擎视为该语句的运行时常量时,该计划将为msTVF表变量结果提供两个单独的运算符:一个用于填充表; 另一个访问结果,可能多次访问表,并可能使用函数定义中声明的索引。
为了突出使用相关参数(外部引用)或非恒定函数参数时的差异,我们将更改表的内容,T
以便该函数还有更多工作要做:
TRUNCATE TABLE dbo.T;
INSERT dbo.T (i)
VALUES (50001), (50002), (50003), (50004), (50005);
现在,以下修改后的查询使用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;
该查询大约需要8秒才能返回如下结果:
注意column中各行之间的时差ts
。该WHERE
子句限制了合理大小的输出的最终结果,但是效率低下的函数仍然需要花费一些时间才能向表变量中填充50,000多行(取决于i
from table 的相关值T
)。
执行计划是:
注意缺少序列运算符。现在,只有一个表值函数运算符可填充表变量,并在嵌套循环联接的每次迭代中返回其行。
需要明确的是:在表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);
再次使用相关参数查询:
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;
这次结果将在1.5秒左右出现:
注意每行上的时间戳相同。table变量中的缓存结果将重新用于相关值i
未更改的后续迭代。重用结果比每次插入50,005行要快得多。
执行计划看起来与以前非常相似:
关键区别是在实际重新绑定和实际倒带表值函数算子性质:
当相关参数不变时,SQL Server可以在表变量中重播(快退)当前结果。当相关性更改时,SQL Server必须截断并重新填充表变量(重新绑定)。一个重新绑定发生在第一次迭代中。由于的值T.i
未更改,因此所有四个后续迭代均倒带。