外部应用在不匹配时意外返回列NOT NULL

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了外部应用在不匹配时意外返回列NOT NULL相关的知识,希望对你有一定的参考价值。

当与OUTER APPLY一起使用时,我在表值函数上遇到了一些奇怪的行为。我有一个简单的内联函数,它返回一些基于另一个表中的行的简单计算。当TVF的输入值是硬编码标量时,不返回任何行。当我在CTE中使用相同的标量并从中生成一行时,然后使用CROSS APPLY将它们作为列提供,没有结果集。当我使用OUTER APPLY执行相同操作时,我得到1行(如预期的那样),但是两个输出列是NULL,另外两个是NOT NULL。根据BOL,这不应该发生在OUTER APPLY。这是用户错误吗?我写了一个简单的版本来演示这个问题。

--Test set-up
CREATE FUNCTION dbo.TVFTest
(
       @keyID INT,
       @matchValue1 MONEY,
       @matchValue2 MONEY
)
RETURNS TABLE AS RETURN
(
WITH TestRow
     AS (SELECT @keyID       AS KeyID,
                @matchValue1 AS MatchValue1,
                @matchValue2 AS MatchValue2)
SELECT KeyID,
       MatchValue1,
       MatchValue2,
       CASE
         WHEN MatchValue1 <> MatchValue2
           THEN 'Not equal'
         ELSE 'Something else'
       END AS MatchTest
FROM   TestRow
WHERE  MatchValue1 <> MatchValue2 
)
GO

询问

WITH Test AS
(
       SELECT 12 AS PropertyID,
              $350000 AS Ap1,
              350000 AS Ap2
)
SELECT LP.*
FROM Test T
OUTER APPLY dbo.TVFTest
(
       T.PropertyID,
       T.Ap1,
       T.Ap2
) LP;

结果

+-------+-------------+-------------+-----------+
| KeyID | MatchValue1 | MatchValue2 | MatchTest |
+-------+-------------+-------------+-----------+
|    12 | 350000.00   | NULL        | NULL      |
+-------+-------------+-------------+-----------+

使用Cross Apply不会按预期返回任何行。同时删除CTE并使用内联常量不会返回任何行。

--Scalars, no row here...
SELECT LP.*
FROM dbo.TVFTest
(
       12,
       $350000,
       350000
) LP;
答案

这肯定是产品中的一个错误。

类似的bug was already reported and closed as "Won't Fix"

包括这个问题,链接的连接项和another two在这个网站上的问题我已经看到了这种类型的行为与内联TVF和OUTER APPLY的四种情况 - 所有这些都是格式

OUTER APPLY dbo.SomeFunction(...) F

写作时返回正确的结果

OUTER APPLY (SELECT * FROM dbo.SomeFunction(...)) F

所以这看起来像是一种可能的解决方法。

对于查询

WITH Test AS
(
       SELECT 12 AS PropertyID,
              $350000 AS Ap1,
              350000 AS Ap2
)
SELECT LP.*
FROM Test T
OUTER APPLY dbo.TVFTest
(
       T.PropertyID,
       T.Ap1,
       T.Ap2
) LP;

执行计划看起来像

enter image description here

并且最终投影中的输出列列表是。 Expr1000,Expr1001,Expr1003,Expr1004。

但是,在右下角的常量表中只定义了其中两列。

文字$350000在右上角的常量表(Expr1001)中定义。然后将其外部连接到右下角的常量表中。由于没有行匹配连接条件,因此在那里定义的两列(Expr1003,Expr1004)被正确评估为NULL。然后最终计算标量将文字12作为新列(Expr1000)添加到数据流中,而不考虑外连接的结果。

这些都不是正确的语义。与内联TVF手动内联时的(正确)计划进行比较。

WITH Test
     AS (SELECT 12      AS PropertyID,
                $350000 AS Ap1,
                350000  AS Ap2)
SELECT LP.*
FROM   Test T
       OUTER APPLY (SELECT KeyID,
                           MatchValue1,
                           MatchValue2,
                           CASE
                             WHEN MatchValue1 <> MatchValue2
                               THEN 'Not equal'
                             ELSE 'Something else'
                           END AS MatchTest
                    FROM   (SELECT T.PropertyID AS KeyID,
                                   T.Ap1        AS MatchValue1,
                                   T.Ap2        AS MatchValue2) TestRow
                    WHERE  MatchValue1 <> MatchValue2) LP 

enter image description here

这里最终投影中使用的列是Expr1003, Expr1004, Expr1005, Expr1006。所有这些都在右下角的恒定扫描中定义。

在TVF的情况下,这一切似乎很早就出错了。

添加OPTION (RECOMPILE, QUERYTRACEON 3604, QUERYTRACEON 8606);显示进程的输入树已经不正确。在SQL中表达它就像。

SELECT Expr1000,
       Expr1001,
       Expr1003,
       Expr1004
FROM   (VALUES (12,
               $350000,
               350000)) V1(Expr1000, Expr1001, Expr1002)
       OUTER APPLY (SELECT Expr1003,
                           IIF(Expr1001 <> Expr1003, 
                               'Not equal', 
                               'Something else') AS Expr1004
                    FROM   (SELECT CAST(Expr1002 AS MONEY) AS Expr1003) D
                    WHERE  Expr1001 <> Expr1003) OA 

该跟踪标志的完整输出如下(并且8605显示基本相同的树。)

*** Input Tree: ***
        LogOp_Project COL: Expr1000  COL: Expr1001  COL: Expr1003  COL: Expr1004 

            LogOp_Apply (x_jtLeftOuter)

                LogOp_Project

                    LogOp_ConstTableGet (1) [empty]

                    AncOp_PrjList 

                        AncOp_PrjEl COL: Expr1000 

                            ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=12)

                        AncOp_PrjEl COL: Expr1001 

                            ScaOp_Const TI(money,ML=8) XVAR(money,Not Owned,Value=(10000units)=(-794967296))

                        AncOp_PrjEl COL: Expr1002 

                            ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=350000)

                LogOp_Project

                    LogOp_Select

                        LogOp_Project

                            LogOp_ConstTableGet (1) [empty]

                            AncOp_PrjList 

                                AncOp_PrjEl COL: Expr1003 

                                    ScaOp_Convert money,Null,ML=8

                                        ScaOp_Identifier COL: Expr1002 

                        ScaOp_Comp x_cmpNe

                            ScaOp_Identifier COL: Expr1001 

                            ScaOp_Identifier COL: Expr1003 

                    AncOp_PrjList 

                        AncOp_PrjEl COL: Expr1004 

                            ScaOp_IIF varchar collate 53256,Var,Trim,ML=14

                                ScaOp_Comp x_cmpNe

                                    ScaOp_Identifier COL: Expr1001 

                                    ScaOp_Identifier COL: Expr1003 

                                ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=9) XVAR(varchar,Owned,Value=Len,Data = (9,Not equal))

                                ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=14) XVAR(varchar,Owned,Value=Len,Data = (14,Something else))

            AncOp_PrjList 

*******************
另一答案

我做了一些进一步的研究(SQL Server 2012) - 这真的很奇怪!

你可以简化这个。在我看来,它与隐式类型转换有关。这就是我尝试使用数据类型的原因......

试试这个:

--Test set-up
CREATE FUNCTION dbo.TVFTest
(
       @ValueInt INT,
       @ValueMoney MONEY,
       @ValueVarchar VARCHAR(10),
       @ValueDate DATE,
       @DateAsVarchar DATE

)
RETURNS TABLE AS RETURN
(
       SELECT @ValueInt AS ValueInt
             ,@ValueMoney AS ValueMoney
             ,@ValueVarchar AS ValueVarchar
             ,@ValueDate AS ValueDate
             ,@DateAsVarchar AS DateAsVarchar
        WHERE 1 != 1
)
GO

由于WHERE,此函数永远不会返回一行...

DECLARE @d AS DATE='20150101';

稍后需要这个类型化的日期变量,尝试在GETDATE()的调用中替换它...

--direct call: comes back with no row
SELECT * FROM dbo.TVFTest(1,2,'test',@d,'20150101');

--parameters via CTE: 
WITH Test AS
(
       SELECT 1 AS valint,
              2 AS valmoney,
              'test' AS valchar,
              @d AS valdate, --try GETDATE() here!
              '20150101' AS valdateasvarchar
)
SELECT * 
FROM Test AS T
OUTER APPLY dbo.TVFTest(T.valint,T.valmoney,T.valchar,T.valdate,T.valdateasvarchar) AS LP;

两个隐式转换的参数(Money和DateAsVarchar)都没有显示,但是INT,VARCHAR和“真正的”DATE都有!!! 查看执行计划:enter image description here此调用是使用GETDATE()完成的。否则只有2个标量运算符......

编辑:执行计划中的第一个“计算标量”显示所有列,恒定扫描(扫描带有常量的内部表)只有两列(如果使用GETDATE()则为三列)。在这个阶段,“坏”栏目似乎不是CTE的一部分......

--parameters via CTE with single calls 
WITH Test AS
(
       SELECT 1 AS valint,
              2 AS valmoney,
              'test' AS valchar,
              @d AS valdate,
              '20150101' AS valdateasvarchar
)
SELECT * FROM dbo.TVFTest((SELECT valint FROM Test)
                         ,(SELECT valmoney FROM Test)
                         ,(SELECT valchar FROM Test)
                         ,(SELECT valdate FROM Test)
                         ,(SELECT valdateasvarchar FROM Test));
GO
DROP FUNCTION dbo.TVFTest;

再试一次,这会返回预期的结果(空)

我的结论:只处理需要一些额外处理的标量值,因此“知道”它们不应该出现。所有可以在没有任何额外工作的情况下传递的标量值都不会在函数内处理并显示 - 这是一个错误。

你怎么看?

以上是关于外部应用在不匹配时意外返回列NOT NULL的主要内容,如果未能解决你的问题,请参考以下文章

如何在不使用溢出隐藏的情况下将边框添加到与边框半径完美匹配的表格列

SQL 查看外部应用速度

如何使用机器人框架和 python 在不滚动的情况下获取所有匹配的元素?

从 excel 导入数据进行访问时,从外部数据库驱动程序 (1) 收到意外错误

InAppBrowser 视图与 ios 中的实际浏览器视图不匹配

如何在不知道匹配返回的 json 数据中的所有值的情况下从 .Net 核心中的 Web api 调用创建模型?