Why is CROSS APPLY so slow? Part 2: Repeating I/O

By
Steven Ensslen
January 22, 2014
Steven-Ensslen-Orange


This is the second post in a series on the performance of functions in SQL Server. The first post describes call overhead.
The real killer in T-SQL functions is that any SELECT statement in the function is executed on every execution. Calling the function over and over will pin the pages that it uses in memory, but there are still significant costs to run even a trivial query against a table in memory.
Putting a SELECT in a function which you are going to CROSS APPLY is just like putting it in a big loop. It does the same work over and over.


DECLARE @counter INT = 0
DECLARE @example INT
WHILE @counter < 1E5
BEGIN
SELECT @example = Count(*)
FROM   information_schema.schemata
SET @counter = @counter + 1
END
If you’re doing some sort of data lookup this overhead be difficult to avoid. If performance is your key consideration, consider using a VIEW or a stored PROCEDURE to hold your code rather than a FUNCTION.
Often I have observed functions that are running the same SELECT with the same parameters on every execution. Usually this is done to read configuration that might change over time, but which is constant for days at a time. I these cases T-SQL doesn’t have the good options which are available some other RDBMS. The only trick that I know of is to implement a session cached cursor.
Here is a function that runs its SELECT on every call:


CREATE FUNCTION [dbo].Ufncursoreverytime (@in INT)
returns @returntable TABLE (
[original]    INT,
[transformed] INT )
AS
BEGIN
DECLARE @value INT
DECLARE confcur CURSOR local fast_forward FOR
SELECT value
FROM   configuration
WHERE  [key] = ( @in % 1000 ) + 1
OPEN confcur
FETCH next FROM confcur INTO @value
INSERT @returntable
SELECT @in,
@in * @value
RETURN
END
And here is that function rewritten to use a shared GLOBAL cursor:


CREATE FUNCTION [dbo].Ufnsharedcursor (@in INT)
returns @returntable TABLE (
[original]    INT,
[transformed] INT )
AS
BEGIN
DECLARE @key INT
DECLARE @value INT
DECLARE @chosen INT
IF Cursor_status(‘global’, ‘ufnSharedCursor_Main’) = –3
BEGIN
DECLARE ufnsharedcursor_main CURSOR scroll read_only FOR
SELECT [key],
value
FROM   configuration
OPEN ufnsharedcursor_main
END
FETCH first FROM ufnsharedcursor_main INTO @key, @value
WHILE ( @@FETCH_STATUS = 0 )
BEGIN
IF @key = ( @in % 1000 ) + 1
BEGIN
SET @chosen = @value
BREAK
END
FETCH next FROM ufnsharedcursor_main INTO @key, @value
END
INSERT @returntable
SELECT @in,
@in * @chosen
RETURN
END
I have used this technique to good effect, but it is fickle. This particular example would not co-operate. The shared cursor version is slower than the unshared version. So be sure to test thoroughly. Steven.

Copyright © 2019 OptimalBI LTD.