Ahem
---
I'm currently trying to replace a scalar UDF with either an inline table valued function, or a multi-statement table valued function, to reduce some performance issues with a large outer query.

The reason for the loop and cursor inside the scalar udf is that there are some calculations that are performed where the current state has to be stored and then used in future iterations of the loop. 

The Repro
--

For maximum realism, this is how the variable number of rows are generated.

```
CREATE OR ALTER FUNCTION dbo.m(@num_rows int)
RETURNS @t table(id int)
AS
BEGIN

    INSERT 
        @t (id)
    SELECT TOP (@num_rows)
        ROW_NUMBER() OVER
        (
            ORDER BY
                1/0
        ) AS n
    FROM sys.messages AS m;

    RETURN;

END;
GO 
```

Testing, testing.

```
SELECT
    m.id
FROM dbo.m(10) AS m;
GO 
```

And now the UDF!
```
CREATE OR ALTER FUNCTION dbo.s(@i int)
RETURNS bigint
AS
BEGIN

/*table*/
DECLARE @t table(id int);
/*nonsense*/
DECLARE @r int = 0;
DECLARE @c int = 0;
DECLARE @p int = 0;
DECLARE @pp int = 0;
/*cursor*/
DECLARE @cid int = 0;
DECLARE @id int = 0;

/*Rows and rows*/
INSERT 
    @t (id)
SELECT
    m.id
FROM dbo.m(@i) AS m;

/*Outer loop*/
WHILE @c <= 1
BEGIN

    /*Cursor nonsense*/
    DECLARE c CURSOR FOR
    SELECT t.id FROM @t AS t;
    OPEN c;
    FETCH c INTO @cid;
    WHILE @@FETCH_STATUS = 0
    BEGIN
    
        /*Some maths*/
        SET @p = @cid + @r - (@pp + @id);
        SET @pp += @p;
        SET @id += @cid;
        
        FETCH c INTO @cid;
    
    END;
    DEALLOCATE c;
    
    /*Some final maths*/
    SET @r += @cid;
    SET @c += 1;

END;

RETURN @r;

END;
GO 
```

Some test runs, too.
```
SELECT
    dbo.s(5) AS s;

SELECT
    dbo.s(10) AS s;

SELECT
    dbo.s(15) AS s;

SELECT
    dbo.s(20) AS s;
```
Top Answer
Paul White
This is a fairly literal, but general, interpretation of the underlying recursive algorithm implemented as an inline table-valued function:

```
CREATE OR ALTER FUNCTION dbo.InlineS
    (@i integer)
RETURNS TABLE
AS
RETURN
WITH 
    TheCursor AS
    (
        -- Number and count the rows in the cursor
        SELECT
            cursor_row_id = CONVERT(integer, ROW_NUMBER() OVER (ORDER BY (SELECT 1/0))),
            cursor_row_count = COUNT_BIG(*) OVER (),
            cid = M.id
        FROM dbo.m(@i) AS M
    ),
    RecursiveCTE AS
    (
        -- Anchor
        SELECT
            recursion_level = 0,
            cursor_row_id = 0,
            -- Variable initialization
            r = 0,
            c = 0,
            p = 0,
            pp = 0,
            id = 0

        UNION ALL

        SELECT 
            recursion_level = R.recursion_level + 1,
            cursor_row_id = New.cursor_row_id,
            r = New.r,
            c = New.c,
            p = New.p,
            pp = R.pp + New.p,
            id = New.id
        FROM RecursiveCTE AS R
        JOIN TheCursor AS TC
            ON TC.cursor_row_id = R.cursor_row_id + 1
        CROSS APPLY 
        (
            SELECT 
                -- The IIF bits separate outer loop logic from cursor loop
                cursor_row_id = IIF(TC.cursor_row_id < TC.cursor_row_count, TC.cursor_row_id, 0),
                r = IIF(TC.cursor_row_id < TC.cursor_row_count, R.r, R.r + TC.cid),
                c = IIF(TC.cursor_row_id < TC.cursor_row_count, R.c, R.c + 1),
                -- Cursor loop only
                p = TC.cid + R.r - (R.pp + R.id),
                id = R.id + TC.cid
        ) AS New
        WHERE
            -- Outer loop limit
            R.c <= 1
    )
SELECT TOP (1)
    -- Debug values:
    --R.recursion_level, 
    --R.cursor_row_id, 
    --R.c, 
    --R.p, 
    --R.pp, 
    --R.id,
    -- Return value:
    R.r 
FROM RecursiveCTE AS R
ORDER BY
    R.recursion_level DESC;
GO
```

It may be inefficient, particularly since it counts the rows in the cursor table on each iteration, but with a maximum of 12 rows in the cursor, this may not be worth spending time on.

It is almost certain that a better inline implementation of the underlying requirement is possible, but that depends on requirement details you are unable to share.

Hopefully, the above code provides a starting point, and some food for thought.

Enter question or answer id or url (and optionally further answer ids/urls from the same question) from

Separate each id/url with a space. No need to list your own answers; they will be imported automatically.