Search code examples
sqlsql-serveruser-defined-functionsnon-deterministic

MSSQL Why is this function non-deterministic


I have this user function, which is always flagged as non-deterministic, although the value will always be the same as long as the input parameter is the same. Everything I've read suggests this should be deterministic.

Can anybody spot why?

    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO

    ALTER FUNCTION [dbo].[udfGetCriteriaScore] 
    (   
        @InputString varchar(max)
    )
    RETURNS int 
    WITH SCHEMABINDING
    AS
    BEGIN
        DECLARE @returnScore int;

            DECLARE @IdentifierChar NCHAR(1)= '"'
            DECLARE @overallScore int
            DECLARE @FirstID int
            DECLARE @SecondID int
            DECLARE @TargetString varchar(MAX)
            DECLARE @startDateCriteria varchar(MAX);
            DECLARE @endDateCriteria varchar(MAX);

            declare @scoringTable table (
                Criterion varchar(max),
                CriteriaScore int,
                Occurances int,
                SumTotalScore int
            )

            DECLARE @TotalCriterions int = (SELECT LEN(@InputString) - LEN(REPLACE(@InputString, '@', '')))
            declare @COUNT int = 0
            declare @Length int = 0

            WHILE(@COUNT) < @TotalCriterions
            BEGIN
                Set @FirstID = CHARINDEX(@IdentifierChar, @InputString, @Length)
                Set @SecondID = CHARINDEX(@IdentifierChar, @InputString, @FirstID + 1)
                Set @Length = @SecondID - @FirstID
                Set @TargetString = SUBSTRING(@InputString, @FirstID + 1, @Length - 1)
                SET @COUNT = @COUNT + 1
                Set @Length = @SecondID + 1

                DECLARE @criteriaScore int
                DECLARE @criteriaCount int

                DECLARE @Criterion varchar(max)
                SET @Criterion = SUBSTRING(@TargetString, 0, CHARINDEX(':', @TargetString))

                -- Calculate date range score
                IF (LOWER(@Criterion) = '@fromdate' OR LOWER(@Criterion) = '@todate')
                    BEGIN
                        IF LOWER(@Criterion) = '@fromdate'
                            SET @startDateCriteria = SUBSTRING(@TargetString, CHARINDEX(':', @TargetString) + 2, LEN(@TargetString) - CHARINDEX(':', @TargetString))

                        IF LOWER(@Criterion) = '@todate'
                            SET @endDateCriteria = SUBSTRING(@TargetString, CHARINDEX(':', @TargetString) + 2, LEN(@TargetString) - CHARINDEX(':', @TargetString))

                        IF @startDateCriteria IS NOT NULL AND @endDateCriteria IS NOT NULL
                            BEGIN
                                SET @criteriaScore = 5
                                SET @criteriaCount = DATEDIFF (dd, @startDateCriteria, @endDateCriteria) 

                                INSERT INTO @scoringTable 
                                    (Criterion, CriteriaScore, Occurances, SumTotalScore) 
                                VALUES 
                                    ('DateRange', @criteriaScore, @criteriaCount, (@criteriaScore * @criteriaCount))
                            END
                    END
                ELSE
                -- Calculate individual criterion score
                    BEGIN

                        SET @criteriaScore =
                            CASE 
                                WHEN LOWER(@Criterion) = '@branchid' THEN 10
                                WHEN LOWER(@Criterion) = '@locationid' THEN 10
                                WHEN LOWER(@Criterion) = '@salesexecid' THEN 1
                                WHEN LOWER(@Criterion) = '@thedate' THEN 5
                                ELSE 1
                            END

                        SET @criteriaCount =
                            (SELECT 
                                    CASE    
                                        WHEN LEN(REPLACE(@TargetString, @Criterion, '')) < 3 THEN 0
                                        ELSE LEN(@TargetString) - LEN(REPLACE(@TargetString, ';', '')) + 1
                                    END
                            )

                        INSERT INTO @scoringTable 
                                (Criterion, CriteriaScore, Occurances, SumTotalScore) 
                            VALUES 
                                (@Criterion, @criteriaScore, @criteriaCount, (@criteriaScore * @criteriaCount))

                    END
            END

            IF EXISTS (SELECT Occurances from @scoringTable where Occurances > 0 AND LOWER(Criterion) in ('@salesexecid', '@locationid'))
                UPDATE @scoringTable SET SumTotalScore = 0 where LOWER(Criterion) = '@branchid'

            set @returnScore = (select SUM(SumTotalScore) from @scoringTable)

        Return @returnScore;
    END

It is designed to split out strings like this:

["@BranchID: 154","@FromDate: 2018-02-01T00:00:00","@ToDate: 2018-02-26T00:00:00","@SalesExecID: "]

and return an overall score based on date range, number of branches included etc.

The following IsDeterministic check is always 0?

SELECT OBJECTPROPERTY(OBJECT_ID('[dbo].[udfGetCriteriaScore]'), 'IsDeterministic')

Solution

  • Other people have the same problem:

    https://www.sqlservercentral.com/Forums/Topic1545616-392-1.aspx

    There, the solution was to convert the string date to an actual date:

    You need to do an explicit CONVERT to use the string literal. I changed your code a little bit to remove the +1

     CREATE TABLE Test(    DayDate DATE,    DayNumber AS (DATEDIFF( DD,
     CONVERT( DATE, '2014-04-30', 120), DayDate)) PERSISTED)
    
     INSERT INTO Test(DayDate) VALUES(GETDATE()) SELECT * FROM Test DROP
     TABLE Test
    

    The same answer explains this is because there are date formats that are non-deterministic and therefore you need to explicitly set a (deterministic) date format for the string conversion to be considered deterministic.