1
declare 
@fromLocalTime TIME = '06:00:00',
@toLocalTime TIME = '06:59:59',
@runDate AS DATETIME2
SET @runDate = GETUTCDATE()
DECLARE @notificationCreatedFromDate AS DATETIME2 = (SELECT DATEADD(day, -1, @runDate))
SELECT 
Col1,
Col2,
Col3,
...,
Global.UDF_ConvertDateUtcToTimeZone(@runDate, tz.TimeZone) AS 'UserLocalTime',
tz.TimeZone
FROM Notification tn
INNER JOIN Dilect d ON d.DilectID = tn.DilectID AND d.IsActive = 1
INNER JOIN NotificationType nt ON nt.NotificationTypeID = tn.NotificationTypeID AND nt.IsActive = 1
INNER JOIN Task t ON t.TaskID = tn.TaskID AND t.IsActive = 1 AND t.ApplicationID = @ApplicationID
INNER JOIN UserTimeZone userTz ON userTz.UserID = tn.UserID AND userTz.ClientId = t.ClientId
INNER JOIN Global.TimeZone tz ON tz.TimeZoneId = userTz.TimeZoneId AND tz.IsActive = 1
WHERE CAST (Global.UDF_ConvertDateUtcToTimeZone(@runDate, tz.TimeZone) AS TIME) BETWEEN @fromLocalTime AND @toLocalTime
AND tn.CompletedDate IS NULL
AND tn.IsActive = 1
AND d.CompletedDate IS NOT NULL
AND tn.Created_DT >= @notificationCreatedFromDate and tn.Created_DT <= @runDate
WHERE CAST (Global.UDF_ConvertDateUtcToTimeZone(@runDate, tz.TimeZone) AS TIME) BETWEEN @fromLocalTime AND @toLocalTime

I have this previously written query which was working fine till yesterday. Suddenly this query taking long time to execute(10 mins now - 10 Seconds previously). I have added suggested index previously on the columns. The issue is due to the highlighted line in the above code(WHERE clause). If I keep that line code then query takes 10 mins to execute and if remove it executes in 10 seconds and both returns 300K records. I have gone through few articles regarding how CAST impacts select statements- but can't relate to the scenarios. I have also tried moving all the code except the highlighted line to outer query and given the highlighted query in where clause but cant see any difference in performance.

Can someone tell me what can be the issue with this line(WHERE clause) and suggest something to fix this.

Shardul
  • 309
  • 1
  • 3
  • 17
  • This sounds like parameter sniffing. I was about to suggest making use of querystore but noticed you tagged SQL Server 2019 and 2012 so no idea what you're *actually* using. you could try a *recompile* hint. Your *where* criteria is also not *sargable*. – Stu May 18 '22 at 19:06
  • Maybe try pulling the UDF out of your query. Put the result of the UDF into a parameter and use that parameter in your query. Just a thought, maybe worth a try – Andrew May 18 '22 at 19:10
  • " have also tried moving all the code except the highlighted line to outer query" sounds backwards to me. Have you tried removing that line and wrapping your existing query in `SELECT * FROM ([existing query]) WHERE CAST (UserLocalTime AS TIME) BETWEEN @fromLocalTime AND @toLocalTime`? – EdmCoff May 18 '22 at 19:17
  • 2
    I really don't think it's the CAST alone that's the problem. Have you considered converting your scalar function to an inline table-valued function? – Aaron Bertrand May 18 '22 at 19:22
  • Have you inspected your execution plan? Please do and add it to the question using "Paste The Plan". – Dale K May 18 '22 at 19:23
  • And please tag only the RDBMS version you are using, not multiple. – Dale K May 18 '22 at 19:32
  • @Stu Its not parameter sniffing. I m actually using sql-server-2019 and yes where is not sargable. – Shardul May 19 '22 at 07:56
  • @EdmCoff I have done exactly as you said but there is no impact on performance. – Shardul May 19 '22 at 08:03
  • @AaronBertrand If I remove that line its getting executed in 10 seconds. I have not tried converting it to an inline table-valued function. I will try it and update here. – Shardul May 19 '22 at 08:10
  • @AaronBertrand Thank you for your suggestion. I have converted function to inline table -valued function and it worked perfectly. If you can add it as an answer I can mark it as Answer. – Shardul May 19 '22 at 14:34
  • First thing I notice is that your where clause doesn't make sense. Why are you getting the current UTC date/time - and then converting that to local time and comparing that to hard-coded times? You are not comparing any columns to those times - regardless, so what good is this part of the where clause? If you are looking to find all rows where the created date is in those hours - then you need to have a column that you are comparing to. – Jeff May 19 '22 at 15:35
  • @Jeff Its real time scenario. And this is just one piece of code out of it. – Shardul May 19 '22 at 17:30

1 Answers1

1

Global.UDF_ConvertDateUtcToTimeZone(@runDate, tz.TimeZone) AS 'UserLocalTime',

It is very difficult for SQL Server to optimize a scalar user-defined function, which generally has to be called for every row. If the function itself is inherently inefficient, your problem just multiplied and will keep multiplying as your data gets larger. Microsoft has tried to address this in modern versions, but there are a lot of restrictions and limitations and they have to fix something in almost every cumulative update.

Instead of relying on the engine to eventually compensate for this inefficiency, a much better strategy is to write inline (not multi-statement!) table-valued functions from the start. This leads to folding the query logic into the outer query and gives SQL Server both better statistics and many more optimization paths to consider.

Let's say your scalar function looks like this:

CREATE FUNCTION dbo.DoAThingToOneRow(@i int)
RETURNS int
AS
BEGIN
  RETURN (SELECT [output] = @i + 1);
END

And you call it like this:

SELECT [object_id], 
       [output] = dbo.DoAThingToOneRow([object_id])
  FROM sys.all_objects;

Change it to this:

CREATE FUNCTION dbo.DoAThingToAllRows(@i int)
RETURNS TABLE WITH SCHEMABINDING
AS
  RETURN (SELECT [output] = @i + 1);

And call it like this:

SELECT o.[object_id], 
       f.[output]
FROM sys.all_objects AS o
CROSS APPLY dbo.DoAThingToAllRows(o.[object_id]) AS f;

I could go into the differences between the plans and execution statistics but you should do this on your own with your own function to get a much more realistic comparison. You can check things like sys.dm_exec_query_stats and sys.dm_exec_function_stats.

Aaron Bertrand
  • 272,866
  • 37
  • 466
  • 490
  • I have one more doubt as to why we need to give column name(tz.TimeZone) in single quotes when join this inline table valued function in our query. "INNER JOIN Global.UDF_InlineTableValuedFunction(@runDate, 'tz.TimeZone')" – Shardul May 19 '22 at 17:28
  • @Shardul you can't use single quotes around the value like that; that passes in the literal string `tz.TimeZone`, not the value that is in the `TimeZone` column. You can't use `INNER JOIN` against a TVF like that if the TVF requires input bound to another object in the query. You need to use `APPLY`, like my example. – Aaron Bertrand May 19 '22 at 17:47
  • 1
    @Shardul As an aside, you do use things like this: `AS 'UserLocalTime'` - you should never use string delimiters to surround something you or SQL Server should be interpreting as an entity. That syntax is deprecated in some forms and really shouldn't be making a column or table look like a string literal (which may be part of the confusion of why you thought single quotes would fix the `multi-part identifier` error). `"double quotes"` are bad for the same reason. If you _have_ to use delimiters (which should only be required if you break identifier rules, like spaces), use `[square brackets]`. – Aaron Bertrand May 19 '22 at 17:53
  • Thanks for the detailed explanation. Will take care of the mentioned things. – Shardul May 19 '22 at 18:09