【问题标题】:Outer Apply Returning columns unexpectedly NOT NULL when no match不匹配时,外部应用返回列意外地不为空
【发布时间】:2015-09-04 22:22:47
【问题描述】:

当与 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;

【问题讨论】:

  • 我相信这是因为您的 TVF WHERE MatchValue1 != MatchValue2 中有一个过滤器,并且您的每个测试都提供了匹配的值。所以它不返回任何匹配项。删除该条件,我认为您将获得预期的结果。 ... nvr mind,可能误解了您的问题...
  • OUTER APPLY 的行为应该类似于针对函数的外部连接。我选择这个例子返回 0 个结果。但是,TVF 返回的所有列都应该为 NULL。第一个不是。我的问题是——为什么?我的代码中有错误还是其他原因?
  • 很高兴你抓住了它。这个问题有点微妙。
  • 您想看到更奇怪的东西,将值转换为金钱,您可以取回除 case 语句之外的每一行:SELECT T.PropertyID, LP.KeyID, LP.MatchValue1, LP.MatchValue2, LP.MatchTest FROM (SELECT 12 AS PropertyID, cast(350000 as money) AS Ap1, cast(350000 as money) AS Ap2) T OUTER APPLY dbo.TVFTest(T.PropertyID, T.Ap1, T.Ap2) LP;
  • 也发生在 SQL Server 2014 上。这是一个错误。您可以通过connect.microsoft.com/SQLServer 举报

标签: sql-server sql-server-2008 tsql sql-server-2012


【解决方案1】:

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

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

包括这个问题,链接的连接项和anothertwo这个网站上的问题我已经看到了四例这种行为的内联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;

执行计划的样子

最终投影中的输出列列表是。 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 

这里最终投影中使用的列是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 

*******************

【讨论】:

  • 谢谢马丁。我很欣赏对此进行的非常彻底的研究。我想我会找到一个解决方法,因为这上面有一个标记为“无法修复”的连接项。同时,我将在此打开一个连接项。谢谢!
  • @CDC - 如果您收到任何有趣的回复,请更新,
【解决方案2】:

我做了一些进一步的研究(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 会显示!!!
看执行计划: 这个调用是用 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;

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

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

你有什么意见?

【讨论】:

  • 感谢您对此进行调查。我感谢所有的挖掘。不过,马丁一针见血,指出了已经为此提交的连接错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-16
相关资源
最近更新 更多