Ran*_*gen 31 sql-server functions sql-server-2017
目的
在尝试创建自引用函数的测试示例时,一个版本失败而另一个版本成功。
唯一的区别是添加SELECT到函数体导致两者的执行计划不同。
起作用的功能
CREATE FUNCTION dbo.test5(@i int)
RETURNS INT
AS
BEGIN
RETURN(
SELECT TOP 1
CASE
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN dbo.test5(1) + dbo.test5(2)
END
)
END;
Run Code Online (Sandbox Code Playgroud)
调用函数
SELECT dbo.test5(3);
Run Code Online (Sandbox Code Playgroud)
退货
(No column name)
3
Run Code Online (Sandbox Code Playgroud)
不起作用的功能
CREATE FUNCTION dbo.test6(@i int)
RETURNS INT
AS
BEGIN
RETURN(
SELECT TOP 1
CASE
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN (SELECT dbo.test6(1) + dbo.test6(2))
END
)END;
Run Code Online (Sandbox Code Playgroud)
调用函数
SELECT dbo.test6(3);
Run Code Online (Sandbox Code Playgroud)
或者
SELECT dbo.test6(2);
Run Code Online (Sandbox Code Playgroud)
结果出现错误
超出最大存储过程、函数、触发器或视图嵌套级别(限制为 32)。
猜测原因
在失败函数的估计计划上有一个额外的计算标量,调用
<ColumnReference Column="Expr1002" />
<ScalarOperator ScalarString="CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END">
Run Code Online (Sandbox Code Playgroud)
而 expr1000 是
<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">
Run Code Online (Sandbox Code Playgroud)
这可以解释超过 32 的递归引用。
实际问题
添加的SELECT使函数一遍又一遍地调用自身,导致无限循环,但为什么添加一个SELECT给出这个结果?
附加信息
Build version:
14.0.3045.24
Run Code Online (Sandbox Code Playgroud)
在兼容性级别 100 和 140 上测试
Pau*_*ite 31
这是项目规范化中的一个错误,通过在具有非确定性函数的 case 表达式中使用子查询来暴露。
为了解释,我们需要预先注意两件事:
CASE是THEN仅当WHEN子句返回 true时才应评估表达式。因此,在有问题的情况下引入的(琐碎的)子查询会导致应用运算符(嵌套循环连接)。为了满足第二个要求,SQL Server 最初将表达式dbo.test6(1) + dbo.test6(2)放在 apply 的内侧:
[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))
Run Code Online (Sandbox Code Playgroud)
...通过连接上的传递谓词CASE遵守语义:
[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)
Run Code Online (Sandbox Code Playgroud)
循环的内侧仅在传递条件评估为假(意思是@i = 3)时才进行评估。到目前为止,这一切都是正确的。在计算标量以下的嵌套循环连接也是荣誉的CASE正确语法:
[Expr1001] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)
Run Code Online (Sandbox Code Playgroud)
问题是查询编译的项目规范化阶段认为这Expr1000是不相关的,并确定将其移到循环之外是安全的(旁白:不是):
[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))
Run Code Online (Sandbox Code Playgroud)
这打破了*传递谓词实现的语义,因此函数在不应该被评估时被评估,并导致无限循环。
你应该报告这个错误。一种解决方法是通过使其相关(即包含@i在表达式中)来防止表达式被移出应用程序,但这当然是一种黑客行为。有一种方法可以禁用项目规范化,但之前有人要求我不要公开分享,所以我不会。
这个问题在 SQL Server 2019标量函数内联时不会出现,因为内联逻辑是直接在解析树上操作的(早在项目规范化之前)。问题中的简单逻辑可以通过内联逻辑简化为非递归:
[Expr1019] = (Scalar Operator((1)))
[Expr1045] = Scalar Operator(CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(int,[Expr1019],0)+(2),0))
Run Code Online (Sandbox Code Playgroud)
...返回 3。
说明核心问题的另一种方法是:
-- Not schema bound to make it non-det
CREATE OR ALTER FUNCTION dbo.Error()
RETURNS integer
-- WITH INLINE = OFF -- SQL Server 2019 only
AS
BEGIN
RETURN 1/0;
END;
GO
DECLARE @i integer = 1;
SELECT
CASE
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN (SELECT dbo.Error()) -- 'subquery'
ELSE NULL
END;
Run Code Online (Sandbox Code Playgroud)
复制从 2008 R2 到 2019 CTP 3.0 的所有版本的最新版本。
Martin Smith提供的另一个示例(没有标量函数):
SELECT IIF(@@TRANCOUNT >= 0, 1, (SELECT CRYPT_GEN_RANDOM(4)/ 0))
Run Code Online (Sandbox Code Playgroud)
这具有所需的所有关键要素:
CASE(在内部实现为ScaOp_IIF)CRYPT_GEN_RANDOM)(SELECT ...))*严格来说,如果对 的评估Expr1000被正确延迟,上述转换仍然是正确的,因为它仅被安全构造引用:
[Expr1002] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)
Run Code Online (Sandbox Code Playgroud)
...但这需要一个内部ForceOrder标志(不是查询提示),它也没有设置。无论如何,项目规范化所应用的逻辑的实现是不正确或不完整的。
SQL Server 的 Azure 反馈站点上的错误报告。