I'm writing a stored procedure with quite a lot of expensive work to do that may or may not take a filter parameter. Doing the filtering is itself quite expensive, and the table being filtered is large. I just tried to change the inner filtering function so throw an error if called with invalid parameters, as a warning to developers not to use it that way.
BUT - If I call my outer test function with NULL, it works as I'd expect, not calling the inner function and not throwing the error. If I call my outer test function with a variable with the VALUE of NULL, then it calls the filter function with a null parameter, and throws the error, even thought the code only says to call the function when the value is not null.
What's going on here?
Much simplified example:
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!
I think what's going on is that in
SELECT * FROM MyTable WHERE (@SearchParameter IS NULL) OR
(@searchParameter IS NOT NULL AND Pk IN (SELECT Pk FROM dbo.FilterRows(@searchParameter)))
The query analyzer can see that the subquery
(SELECT Pk FROM dbo.FilterRows(@searchParameter))
does not depend on any values from MyTable. As it's constant for all rows, it runs that subquery first, in order to join MyTable to the results. So it executes it before evaluating the WHERE clause where it tests whether @searchParameter IS NULL or not.
When @searchParameter is just "NULL" and not a variable with value NULL, then the analyzer can short-circuit the whole where clause in the execution plan and so knows not to pre-calculate the subquery.
Or, something like that.