• You could use a single query like below. I have not tested it against a 4.5 million row input set, but it's probably faster then the loop and all the separate queries you are using right now. I recommend to include the column LinkRL4 to the non-clustered index on ProcessDate. If the zero-offset row numbering in case of a previous row outside the data range is not important, remove the CASE expression.

    SELECT

    evt.*,

    RowNumber = ROW_NUMBER() OVER (PARTITION BY evt.LinkRL4 ORDER BY evt.L4Ident) -

    CASE WHEN MIN(evt.ProcessDate) OVER (PARTITION BY evt.LinkRL4) < @StartDate THEN 1 ELSE 0 END

    FROM

    dbo.#Events evt

    WHERE

    evt.ProcessDate <= @EndDate

    AND NOT EXISTS

    (

    SELECT

    *

    FROM

    dbo.#Events evt2

    WHERE

    evt2.LinkRL4 = evt.LinkRL4

    AND evt2.ProcessDate > evt.ProcessDate

    AND evt2.ProcessDate < @StartDate

    )

    ORDER BY

    evt.LinkRL4, evt.ProcessDate, evt.L4Ident