• For the set-based, tally table, CTE freaks among us, here is a set-based solution that uses a tally table and CTEs:

    SET NOCOUNT ON;

    -- The @Strings table variable represents whatever your source table is.

    DECLARE @Strings TABLE ([StringKey] INT NOT NULL IDENTITY(1, 1),

    [RawString] VARCHAR(1000) NOT NULL,

    PRIMARY KEY CLUSTERED ([StringKey])

    );

    INSERT INTO @Strings ( [RawString] ) VALUES ( '' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( 'abc' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( '789' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( 'abc789' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( '789abc' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( 'abc789abc' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( '123rgt456tgb789asd' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( 'abc123rgt456tgb789' );

    INSERT INTO @Strings ( [RawString] ) VALUES ( 'abc123rgt456tgb789asd' );

    -- we create 6 Common Table Expressions (CTEs)

    -- The first 3 are solely for the purpose of creating a tally table of 1,000 sequential numbers.

    -- If you already have a tally table defined in your database you use it in the [Characters] CTE and remove the first 3 CTEs

    -- The [Characters] CTE breaks each character of each string down into its own row with a value ([IsDigit]) indicating whether it is a numeric character or not

    -- The [LastNumerics] CTE finds the location of the last numeric character in each string

    -- The [[LastNonNumerics]] CTE finds the location of the last non-numeric character in each string that occurs BEFORE the last numeric character

    WITH [First_10]([Dummy]) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    ), -- first 10 rows

    [Full_1000]([Dummy]) AS (SELECT 1 FROM [First_10] a CROSS JOIN [First_10] b CROSS JOIN [First_10] c), -- 10 x 10 x 10 = 1000 rows

    [Tally_1000]([SeqNo]) AS (SELECT ROW_NUMBER() OVER (ORDER BY [Dummy]) FROM [Full_1000]),

    [Characters] AS (SELECT .[StringKey], [sn].[SeqNo], ISNUMERIC(SUBSTRING(.[RawString], [sn].[SeqNo], 1)) AS [IsDigit]

    FROM @Strings s INNER JOIN [Tally_1000] AS sn ON [sn].[SeqNo] <= LEN(.[RawString])

    WHERE [sn].[SeqNo] <= LEN(.[RawString])

    ),

    [LastNumerics] AS (SELECT [c].[StringKey], MAX([c].[SeqNo]) AS [LastNumeric]

    FROM [Characters] AS c

    WHERE [c].[IsDigit] = 1

    GROUP BY [c].[StringKey]

    ),

    [LastNonNumerics] AS (SELECT [c].[StringKey], MAX([c].[SeqNo]) AS [LastNonNumeric]

    FROM [Characters] AS c INNER JOIN [LastNumerics] n ON [n].[StringKey] = [c].[StringKey]

    AND [c].[SeqNo] < [n].[LastNumeric]

    WHERE [c].[IsDigit] = 0

    GROUP BY [c].[StringKey]

    )

    -- Now we can do our SELECT query to extract the characters from each string starting right after the last non-numeric character and going through the last numeric character.

    SELECT .[RawString], SUBSTRING(.[RawString], ISNULL([nn].[LastNonNumeric], -1) + 1, ISNULL([n].[LastNumeric], 0) - ISNULL([nn].[LastNonNumeric], -1)) AS [Result]

    FROM @Strings s LEFT OUTER JOIN [LastNumerics] n ON [n].[StringKey] = .[StringKey]

    LEFT OUTER JOIN [LastNonNumerics] nn ON [nn].[StringKey] = .[StringKey];