Search code examples
sqlsql-servert-sqlcursor

Removal of Cursor in SQL


Its always been discouraged to use cursor, which have been extensively used in our current stored procedures and replace them with set based queries. But this particular scenario is one, where I did not get solution to use set based query and forced to keep using the cursor. I am providing below sample code which represents the scenario:

DECLARE @temp varchar(10), @continuechar varchar(10)
DECLARE  @table1 table (col1 varchar(10)) 

insert into @table1
select 'A' UNION
select 'B' UNION
select 'C' UNION
select 'D' UNION
select 'E' UNION
select 'F' UNION
select 'G' 

DECLARE Cursor1 CURSOR for select Col1 from @table1
open Cursor1

FETCH NEXT from Cursor1 into @temp
WHILE @@FETCH_STATUS = 0  
BEGIN
 if @temp='A'
 BEGIN
    set @continuechar=@temp  
 END
 if @temp='C'
 BEGIN
    set @continuechar=@temp
 END
select @continuechar, @temp
FETCH NEXT from Cursor1 into @temp
END
CLOSE cursor1;
deallocate cursor1

Here in above sample code @continuechar variable is not getting set, every time cursor is getting executed. If @continuechar is getting set, then following select statement is providing result set with current value of @continuechar:

select @continuechar, @temp

if its not getting set, then its using the previously set value to provide result set. Can we have set based queries to remove cursor from such scenario.


Solution

  • First I would add some id column to get stable sort. Then simply use windowed functions:

    • SUM() OVER() to calculate groups
    • FIRST_VALUE() OVER() to propagate first value across group (present from SQL Server 2012, you could exchange it with MAX(continuechar) OVER(PARTITION BY grp) if necessary)
    DECLARE  @table1 table (id INT IDENTITY(1,1), col1 varchar(10)) 
    
    insert into @table1
        select 'A' UNION
        select 'B' UNION
        select 'C' UNION
        select 'D' UNION
        select 'E' UNION
        select 'F' UNION
        select 'G';
    
    WITH cte AS (
       SELECT id
            ,col1
            ,CASE WHEN col1 IN('A', 'C') THEN col1 END AS continuechar
            ,SUM(CASE WHEN col1 IN ('A', 'C') THEN 1 ELSE 0 END)
                      OVER(ORDER BY id) AS grp
       FROM @table1
    )
    SELECT id, col1,
       FIRST_VALUE(continuechar) OVER(PARTITION BY grp ORDER BY id) AS continuechar
    FROM cte
    ORDER BY id;
    

    DBFiddle Demo


    EDIT:

    Quirky update This is for pure demo only. Do not use this method on production system:

    DECLARE  @table1 table (id INT IDENTITY(1,1) PRIMARY KEY, col1 varchar(10),
                            continue_char VARCHAR(10));
    DECLARE @temp VARCHAR(10);
    
    insert into @table1(col1)
    select 'A' UNION
    select 'B' UNION
    select 'C' UNION
    select 'D' UNION
    select 'E' UNION
    select 'F' UNION
    select 'G';
    
    UPDATE @table1
    SET   @temp = CASE WHEN col1 IN ('A','C') THEN col1 ELSE @temp END
         ,continue_char = @temp
    OPTION(MAXDOP 1);
    
    SELECT *
    FROM @table1;
    

    DBFiddle Demo2