udf very slow?

  • Jeff Moden (10/31/2013)


    Eric Mamet (10/31/2013)


    I know but I was hoping it would be fast...

    Just talked to my customer and I'll try using CLR!

    Yipee!!! 😀

    Heh... hate to throw a wet blanket on your fire but you don't need to resort to an SQLCLR function for something so simple (although if you're comfortable with that, then fire away! It can be a great solution when done properly.:-D). Please see the example that Sean Pierce provided in his post above and please see the following article that demonstrates the problem and the fix (the "iSF").

    How to Make Scalar UDFs Run Faster (SQL Spackle)

    [/url]

    Looks like we have been reading the same articles 😉

    Alan.B (10/31/2013)


    ...Take a look at this article How to Make Scalar UDFs Run Faster (SQL Spackle)[/url]. As Sean was saying and demonstrated...

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • Alan.B (11/1/2013)


    Looks like we have been reading the same articles 😉

    More like he wrote the article you read 😛

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Hi everyone, sorry for being so slow but I just did not have time to go through the article before...

    Yes, it is very interesting and intriguing but it looks like I won't be able to use that due to the complexity of my UDF.

    CREATE FUNCTION dbo.udf_ResidualValue(@RVDate DATETIME

    , @GARMENTSTATUS INT

    , @RVMATRIXCODE VARCHAR(10)

    , @LIFETIME INT

    , @GARMENTISSUEDATE DATETIME

    , @STARTPRICE NUMERIC(28,12)

    , @ENDPRICE NUMERIC(28,12)

    , @TERMINATIONDISCPCT NUMERIC(28,12)

    , @STS_CONTINUOUSCHARGING INT

    , @WEARERSTATUS INT

    , @SETQTY INT

    , @GarmentNumber INT

    )

    RETURNS DECIMAL(9,2)

    AS

    BEGIN

    IF ( @RVDate < @GARMENTISSUEDATE )

    RETURN 0.0;

    IF @SETQTY > 0

    AND @GarmentNumber > @SETQTY

    RETURN 0.0;

    IF LEN(LTRIM(@RVMATRIXCODE)) = 0

    RETURN 0.0;

    IF (@TERMINATIONDISCPCT = 100)

    RETURN 0.0;

    DECLARE @GarmentAgeInMonth INT

    DECLARE @DateX DATE

    DECLARE @DateY DATE

    DECLARE @Sign INT

    IF(@GARMENTISSUEDATE < @RVDate)

    BEGIN

    SET @DateX = @GARMENTISSUEDATE

    SET @DateY = @RVDate

    SET @Sign = 1

    END

    ELSE

    BEGIN

    SET @DateX = @RVDate

    SET @DateY = @GARMENTISSUEDATE

    SET @Sign = -1

    END

    SELECT @DateY = DATEADD(day, 1, @DateY);

    SET @GarmentAgeInMonth = @Sign * (

    SELECT

    CASE

    WHEN DATEPART(DAY, @DateX) = DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY)

    WHEN DATEPART(DAY, @DateX) = DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY)

    WHEN DATEPART(DAY, @DateX) >= DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY) - 1

    ELSE DATEDIFF(MONTH, @DateX, @DateY)

    END

    )

    DECLARE @RVRatio FLOAT

    IF @TERMINATIONDISCPCT IS NULL

    SET @RvRatio = 1.0;

    ELSE

    SET @RvRatio = ( 100.00 - @TERMINATIONDISCPCT ) / 100.00;

    IF @LIFETIME > 0 AND @GarmentAgeInMonth <= @LIFETIME

    BEGIN

    -- Calculate the monthly depreciation to 3 digits to imitate the exact Axapta calculation

    DECLARE @MonthlyDepreciation DECIMAL(18, 3)

    SELECT @MonthlyDepreciation = (@StartPrice - @EndPrice) / @LIFETIME;

    RETURN CAST( @StartPrice - (@MonthlyDepreciation * @GarmentAgeInMonth ) AS DECIMAL(28, 2)) * @RVRatio;

    END

    IF @STS_CONTINUOUSCHARGING = 1

    AND @LIFETIME > 0

    AND @GarmentAgeInMonth > @LIFETIME

    RETURN @ENDPRICE * @RVRatio;

    RETURN 0.0;

    END

    In fact, there are even business type arguments around this function so I wrote another one returning the reason for the value rather than the value itself so that I can explain my results as well.

    Having said that, I have learnt some very interesting stuff about UDFs.

    Thanks

    Eric

  • I am pretty sure that you can turn that into an inline table valued function. It isn't something I can do quickly but I will take a shot at this later today unless someone beats me to it.

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • Eric Mamet (11/7/2013)


    Hi everyone, sorry for being so slow but I just did not have time to go through the article before...

    Yes, it is very interesting and intriguing but it looks like I won't be able to use that due to the complexity of my UDF.

    CREATE FUNCTION dbo.udf_ResidualValue(@RVDate DATETIME

    , @GARMENTSTATUS INT

    , @RVMATRIXCODE VARCHAR(10)

    , @LIFETIME INT

    , @GARMENTISSUEDATE DATETIME

    , @STARTPRICE NUMERIC(28,12)

    , @ENDPRICE NUMERIC(28,12)

    , @TERMINATIONDISCPCT NUMERIC(28,12)

    , @STS_CONTINUOUSCHARGING INT

    , @WEARERSTATUS INT

    , @SETQTY INT

    , @GarmentNumber INT

    )

    RETURNS DECIMAL(9,2)

    AS

    BEGIN

    IF ( @RVDate < @GARMENTISSUEDATE )

    RETURN 0.0;

    IF @SETQTY > 0

    AND @GarmentNumber > @SETQTY

    RETURN 0.0;

    IF LEN(LTRIM(@RVMATRIXCODE)) = 0

    RETURN 0.0;

    IF (@TERMINATIONDISCPCT = 100)

    RETURN 0.0;

    DECLARE @GarmentAgeInMonth INT

    DECLARE @DateX DATE

    DECLARE @DateY DATE

    DECLARE @Sign INT

    IF(@GARMENTISSUEDATE < @RVDate)

    BEGIN

    SET @DateX = @GARMENTISSUEDATE

    SET @DateY = @RVDate

    SET @Sign = 1

    END

    ELSE

    BEGIN

    SET @DateX = @RVDate

    SET @DateY = @GARMENTISSUEDATE

    SET @Sign = -1

    END

    SELECT @DateY = DATEADD(day, 1, @DateY);

    SET @GarmentAgeInMonth = @Sign * (

    SELECT

    CASE

    WHEN DATEPART(DAY, @DateX) = DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY)

    WHEN DATEPART(DAY, @DateX) = DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY)

    WHEN DATEPART(DAY, @DateX) >= DATEPART(DAY, @DateY) THEN DATEDIFF(MONTH, @DateX, @DateY) - 1

    ELSE DATEDIFF(MONTH, @DateX, @DateY)

    END

    )

    DECLARE @RVRatio FLOAT

    IF @TERMINATIONDISCPCT IS NULL

    SET @RvRatio = 1.0;

    ELSE

    SET @RvRatio = ( 100.00 - @TERMINATIONDISCPCT ) / 100.00;

    IF @LIFETIME > 0 AND @GarmentAgeInMonth <= @LIFETIME

    BEGIN

    -- Calculate the monthly depreciation to 3 digits to imitate the exact Axapta calculation

    DECLARE @MonthlyDepreciation DECIMAL(18, 3)

    SELECT @MonthlyDepreciation = (@StartPrice - @EndPrice) / @LIFETIME;

    RETURN CAST( @StartPrice - (@MonthlyDepreciation * @GarmentAgeInMonth ) AS DECIMAL(28, 2)) * @RVRatio;

    END

    IF @STS_CONTINUOUSCHARGING = 1

    AND @LIFETIME > 0

    AND @GarmentAgeInMonth > @LIFETIME

    RETURN @ENDPRICE * @RVRatio;

    RETURN 0.0;

    END

    In fact, there are even business type arguments around this function so I wrote another one returning the reason for the value rather than the value itself so that I can explain my results as well.

    Having said that, I have learnt some very interesting stuff about UDFs.

    Thanks

    Eric

    I am wont have time today but will take a stab at this tonight. In the meantime, to demonstrate how you would convert yours into an iTVF, I put this example together. It demonstrates the techniques that you would use. Note my comments:

    -- Scalar Valued Function

    CREATE FUNCTION dbo.SVF(@x int, @y int, @d date)

    RETURNS int AS

    BEGIN

    DECLARE @return_value int;

    IF @d>getdate()

    RETURN 0;

    IF @x=0

    RETURN 0;

    SET @x=@x+(2*@y)

    SET @y=@y*2;

    IF @x>@y

    BEGIN

    SET @return_value=@x*3

    END

    ELSE

    BEGIN

    SET @return_value=@y

    END

    RETURN @return_value

    END

    GO

    -- the new inline table valued function version (aka iSVF)

    CREATE FUNCTION dbo.iSVF(@x int, @y int, @d date)

    RETURNS TABLE

    AS

    RETURN

    WITH aggreagations AS

    (SELECTx = @x+(2*@y),

    y = @y*2,

    d = @d)

    SELECT return_value=

    CASE WHEN x>y THEN x*3 ELSE @y END *

    CASE WHEN @d>getdate() THEN 0 ELSE 1 END *

    CASE WHEN @x=0 THEN 0 ELSE 1 END

    FROM aggreagations;

    GO

    --test the functions and look at the query plans

    SELECT * FROM dbo.iSVF(1,2,'1/1/2001')

    SELECT dbo.SVF(1,2,'1/1/2001')

    --using iSVF

    CREATE TABLE #sample

    (s_id int primary key,x int not null, y int not null, d date not null);

    --sample data

    INSERT INTO #sample

    SELECT TOP 1000

    ROW_NUMBER() OVER (ORDER BY (SELECT null)),

    1.0+floor(10*RAND(convert(varbinary, newid())))-1,

    1.0+floor(14*RAND(convert(varbinary, newid()))),

    getdate()-4000+(1.0+floor(5000*RAND(convert(varbinary, newid()))))

    FROM master..spt_values;

    -- old way

    SELECT s_id, x,y,d,dbo.SVF(s.x,s.y,s.d) AS val

    FROM #sample s

    --new way

    SELECT s_id, x,y,d,return_value

    FROM #sample s

    CROSS APPLY dbo.iSVF(s.x,s.y,s.d)

    Edit: added s_id to select statements.

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • Almost there... This was the first draft and, though it is processing all your variables, I made a mistake(s) somewhere in my formulas; I had very limited time. The reason I posted this was to demonstrate that, YES, you can turn your function into an inline Table Valued Function. I will post an updated & corrected version when I get some more time.

    CREATE FUNCTION dbo.udf_ResidualValue_iSVF

    (@RVDate DATETIME

    , @GARMENTSTATUS INT

    , @RVMATRIXCODE VARCHAR(10)

    , @LIFETIME INT

    , @GARMENTISSUEDATE DATETIME

    , @STARTPRICE NUMERIC(28,12)

    , @ENDPRICE NUMERIC(28,12)

    , @TERMINATIONDISCPCT NUMERIC(28,12)

    , @STS_CONTINUOUSCHARGING INT

    , @WEARERSTATUS INT

    , @SETQTY INT

    , @GarmentNumber INT)

    RETURNS TABLE

    AS

    RETURN

    WITH

    dates(DateX,DateY,[sign]) AS

    (

    SELECTDateX =CASE

    WHEN (@GARMENTISSUEDATE < @RVDate) THEN @GARMENTISSUEDATE

    ELSE @RVDate

    END,

    DateY =CASE

    WHEN (@GARMENTISSUEDATE < @RVDate) THEN @RVDate

    ELSE @GARMENTISSUEDATE

    END,

    [sign]= CASE

    WHEN (@GARMENTISSUEDATE < @RVDate) THEN 1 ELSE -1

    END),

    age_ratio(GarmentAgeInMonth,RVRatio) AS

    (

    SELECTGarmentAgeInMonth = [Sign] * (

    SELECT

    CASE

    WHEN DATEPART(DAY,DateX) = DATEPART(DAY,DATEADD(day,1,DateY))

    THEN DATEDIFF(MONTH,DateX, DATEADD(day,1, DateY))

    WHEN DATEPART(DAY,DateX) = DATEPART(DAY,DATEADD(day,1,DateY))

    THEN DATEDIFF(MONTH, DateX, DATEADD(day,1, DateY))

    WHEN DATEPART(DAY, DateX) >= DATEPART(day,DATEADD(day,1,DateY))

    THEN DATEDIFF(MONTH, DateX, DATEADD(day, 1, DateY)) - 1

    ELSE DATEDIFF(MONTH, DateX, DATEADD(day, 1, DateY))

    END),

    RVRatio = ISNULL(@TERMINATIONDISCPCT,1.0)*((100.00 - @TERMINATIONDISCPCT) / 100.00)

    FROM dates

    )

    SELECT return_value =

    CAST((CASE

    WHEN @LIFETIME > 0 AND GarmentAgeInMonth <= @LIFETIME

    THEN

    CAST( @StartPrice -

    (CAST(((@StartPrice - @EndPrice) / @LIFETIME) AS DECIMAL(18, 3)) *

    GarmentAgeInMonth ) AS DECIMAL(28, 2)) * RVRatio

    WHEN @STS_CONTINUOUSCHARGING = 1

    AND @LIFETIME > 0

    AND GarmentAgeInMonth > @LIFETIME

    THEN @ENDPRICE * RVRatio

    ELSE 0

    END *

    CASE

    WHEN( @RVDate < @GARMENTISSUEDATE )

    OR(@SETQTY > 0 AND @GarmentNumber > @SETQTY)

    OR(LEN(LTRIM(@RVMATRIXCODE)) = 0)

    OR(@TERMINATIONDISCPCT = 100)

    THEN 0 ELSE 1

    END) AS DECIMAL(9,2))

    FROM age_ratio;

    I don't write scalar valued functions any more. My experience has been that the iTVF version is always equal in speed or faster; especially when there are many 4+ CPUs involved and the only thing preventing parallelism is a SVF (as Louis made reference to). Have a look at this, see the tricks I used to turn it into an iTVF and, in the future, write your functions as iTVFs.

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • Alan,

    I really appreciate your efforts but I would not want to waste your time.

    I can see that it is probably feasible to turn this into the right form but I have strong reservations as whether it is desirable.

    I feel a big challenge in writing any code is to try and make it both to understand and maintain.

    As far as possible, I am trying to split my code in little bits easy to grasp (with lots of comments which I had removed in my sample for space reason).

    In this case, going through the TSQL inline table function approach means grouping all the code into one unique (and therefore difficult to test) big blob of code, all of which to try and work around serious limitations in the (scalar) UDF implementation.

    I am away from my desk for the next few days so I don't have access to my code but I will try to show you the equivalent C# code which IMHO seems way easier to understand/debug.

    On the other hand, this would mean introducing a technology (CLR function) that my client is not (yet) familiar with.

    When I get a minute, I will do another performance test (regardless of the exact values returned) between the CLR and inline table functions just to see if that tips the scale one way or another.

    Once again, a big thank you for your efforts but please hold on fire until I do these speed tests.

    Kind Regards

    Eric 😀

    PS: I am very interested in the Inline table function but I might use it for simpler requests...

Viewing 7 posts - 16 through 21 (of 21 total)

You must be logged in to reply to this topic. Login to reply