添加选择时超出自引用标量函数嵌套级别


24

目的

尝试创建自引用功能的测试示例时,一个版本失败,而另一个版本成功。

唯一的区别是添加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;

调用函数

SELECT dbo.test5(3);

退货

(No column name)
3

该功能不起作用

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;

调用函数

SELECT dbo.test6(3);

要么

SELECT dbo.test6(2);

导致错误

超过最大存储过程,函数,触发器或视图嵌套级别(限制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">

而expr1000被

<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">

这可以解释递归引用超过32。

实际问题

增加的内容SELECT使函数反复调用自身,从而导致无限循环,但是为什么要增加SELECT结果呢?


附加信息

估计执行计划

DB <>提琴

Build version:
14.0.3045.24

经过兼容性级别100和140的测试

Answers:


26

这是项目规范化中的错误,可通过在具有非确定性函数的case表达式内使用子查询来暴露。

为了说明,我们需要预先注意两件事:

  1. SQL Server无法直接执行子查询,因此它们总是被展开或转换为apply
  2. 的语义CASE是,THEN仅当WHEN子句返回true时,才应评估表达式。

因此,在有问题的情况下引入的(琐碎的)子查询会导致应用操作符(嵌套循环联接)。为了满足第二个要求,SQL Server首先将表达式dbo.test6(1) + dbo.test6(2)放在apply的内侧:

突出显示的计算标量

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

...具有联接上的传递谓词所CASE尊重的语义:

[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)

仅当传递条件评估为false(表示@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)

问题在于查询编译的项目规范化阶段发现这Expr1000是不相关的,并确定将其移到循环外是安全的(旁白:不是):

移动项目

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

这破坏了传递谓词实现的语义,因此在不应该使用该函数时对其求值,并导致无限循环。

您应该报告此错误。一种解决方法是通过使@i表达式关联(例如,将其包含在表达式中)来防止将该表达式移出应用程序,但这当然是一个技巧。有一种方法可以禁用项目规范化,但是在不公开共享之前,我曾被问过,所以我不会。

内联标量函数时,在SQL Server 2019中不会出现此问题,因为内联逻辑直接在解析的树上运行(远在项目规范化之前)。问题的简单逻辑可以通过对非递归的内联逻辑进行简化:

[Expr1019] = (Scalar Operator((1)))
[Expr1045] = Scalar Operator(CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(int,[Expr1019],0)+(2),0))

...返回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;

复制从2008 R2到2019 CTP 3.0的所有版本的最新版本。

Martin Smith提供的另一个示例(无标量函数):

SELECT IIF(@@TRANCOUNT >= 0, 1, (SELECT CRYPT_GEN_RANDOM(4)/ 0))

这具有所需的所有关键要素:

  • 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)

...但是这需要内部ForceOrder标志(不是查询提示),该标志也未设置。在任何情况下,项目规范化所应用的逻辑的实现都是不正确或不完整的。

SQL Server的Azure反馈站点上的错误报告

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.