传递值为 null 的变量与传递常量 null 时,Sql Server UDF 的行为不同

Posted

技术标签:

【中文标题】传递值为 null 的变量与传递常量 null 时,Sql Server UDF 的行为不同【英文标题】:Sql Server UDF behaves differently when a variable with value null is passed than when constant null is passed 【发布时间】:2017-01-16 11:00:03 【问题描述】:

我正在编写一个存储过程,其中包含大量昂贵的工作来完成,可能会或可能不会使用过滤器参数。做过滤本身是相当昂贵的,而且被过滤的表很大。我只是尝试更改内部过滤功能,因此如果使用无效参数调用会引发错误,以警告开发人员不要以这种方式使用它。

但是 - 如果我用 NULL 调用我的外部测试函数,它会按我的预期工作,而不是调用内部函数并且不会抛出错误。如果我用一个值为 NULL 的变量调用我的外部测试函数,那么它会用一个 null 参数调用过滤器函数,并抛出错误,即使代码只说在值不为 null 时调用该函数。

这是怎么回事?

非常简化的例子:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND type in (N'U')) DROP TABLE MyTable 
GO

CREATE TABLE MyTable (Pk int, Field int)
GO

INSERT INTO MyTable VALUES (1, 1)
INSERT INTO MyTable VALUES (2, 4)
INSERT INTO MyTable VALUES (3, 9)
INSERT INTO MyTable VALUES (4, 16)
GO

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FilterRows]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT')) DROP FUNCTION FilterRows
GO
CREATE FUNCTION FilterRows(@searchParameter int)
RETURNS @Pks TABLE 
    (           
        Pk int
    )
AS 
BEGIN
    IF (@searchParameter IS null)
    BEGIN
        -- This is bad news. We don't want to be here with a null search, as the only thing we can do is return every row in the whole table
        -- RAISERROR ('Avoid calling FilterRows with no search parameter', 16, 1)       
        -- we can't raise errors in functions!
        -- Make it divide by zero instead then
        INSERT INTO @Pks SELECT Pk FROM MyTable WHERE 1/0 = 1
    END
    ELSE
    BEGIN
        INSERT INTO @Pks SELECT Pk FROM MyTable WHERE Field > @searchParameter
    END
    RETURN
END
GO


IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[OuterFunction]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT')) DROP FUNCTION OuterFunction
GO
CREATE FUNCTION OuterFunction(@searchParameter int)
RETURNS TABLE AS
RETURN 
SELECT * 
FROM 
MyTable
WHERE
(@SearchParameter IS NULL) OR (@searchParameter IS NOT NULL AND Pk IN (SELECT Pk FROM dbo.FilterRows(@searchParameter)))
GO

SELECT * FROM dbo.OuterFunction(2) -- Returns filtered values
SELECT * FROM dbo.OuterFunction(null) -- returns everything, doesn't call FilterRows
DECLARE @x int = null
SELECT * FROM dbo.OuterFunction(@x) -- WTF! Throws error!

【问题讨论】:

“可选过滤器”是一个设计错误,因为优化器会缓存为 first 调用生成的执行计划。而不是短路,你最终会得到更差的性能。 ORM 和 LINQ 已经不再需要这些东西了,它们使用必要的过滤器生成查询。无论您执行即席查询还是等效的存储过程,性能都完全相同 PS 过滤与昂贵相反 - 它减少结果。如果不是,则查询有问题。如果您有正确的索引,更多的条件会导致 更快 的执行。 “短路”尝试会导致执行变慢,因为他们必须处理所有事情才能找到要...电死的东西 为了提出一个简洁的问题,我特意简化了情况。我有充分的理由去做我正在做的事情,而且我知道查询分析器是如何工作的。我要问的问题是关于 UDF 的意外行为。我并不是要求被傲慢地告知我想要达到的目标是错误的。 那么您应该发布问题文本本身存在错误,包括完整错误,而不是代码第二页的最后一行。尝试使用“可选”过滤器参数是一个常见的错误,我在意识到后果之前也犯了这个错误。 错误发生在哪里 好的。我真正拥有的是一个代表文件夹的表,所以主表有一个文件夹 id 和一个父文件夹 id。我正在为报告编写查询,报告可以有一个目标文件夹,在这种情况下,它应该针对该文件夹运行并包括其所有子项和内容。 【参考方案1】:

传递值 null 与传递常量 null 的区别是使用 (is Null) 和 (= null) 之间的区别

@var = null -- considered as false

@var is null -- considered as unknown

更多详情:SQL is null and = null

因此,如果您想让两者的行为(调用常量 null 并传递 Null 值)相同,请使用以下技巧,尽管我不喜欢这个。

将 FilterRows 函数更改为

IF (@searchParameter = null)
--IF (@searchParameter is null)

注意:很抱歉在这里输入这个答案,它应该是评论而不是答案,规则是“你必须有 50 个声望才能评论”,而我只有 22 个 :(

【讨论】:

我认为这不对。声明@x int = NULL; IF (@x IS NULL) PRINT 'X is null' 打印 X 为 null。因为测试 @x IS NULL 返回 true。测试 @x = NULL 返回 null。 IF (@searchParameter = null) 永远不会返回 true,AFAIK。如果你有一个代码 sn-p 显示,否则我会被打扰! 我在使用上面的示例(位于您的问题中)发布之前测试了我的答案,并且您在评论中的代码也是正确的,有些东西是有线的!【参考方案2】:

我认为发生了什么是在

SELECT * FROM MyTable WHERE (@SearchParameter IS NULL) OR 
(@searchParameter IS NOT NULL AND Pk IN (SELECT Pk FROM dbo.FilterRows(@searchParameter)))

查询分析器可以看到子查询

(SELECT Pk FROM dbo.FilterRows(@searchParameter))

不依赖于 MyTable 中的任何值。由于它对于所有行都是恒定的,因此它首先运行该子查询,以便将 MyTable 加入结果。所以它在评估 WHERE 子句之前执行它,它测试 @searchParameter 是否为空。

当@searchParameter 只是“NULL”而不是值为NULL 的变量时,分析器可以将执行计划中的整个where 子句短路,因此知道不预先计算子查询。

或者,类似的东西。

【讨论】:

以上是关于传递值为 null 的变量与传递常量 null 时,Sql Server UDF 的行为不同的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis传递map参数时,key值为空的处理

webservice调用的时候传递参数传值为null。。。为啥呢?

当将字符串变量传递给jasonparser时,jsonobject返回null

传递和命名变量

Laravel:我正在尝试将变量从视图传递到控制器,但它返回 null

内置常量