13

In T-SQL, when iterating results from a cursor, it seems to be common practice to repeat the FETCH statement before the WHILE loop. The below example from Microsoft:

DECLARE Employee_Cursor CURSOR FOR
SELECT EmployeeID, Title FROM AdventureWorks2012.HumanResources.Employee
    WHERE JobTitle = 'Marketing Specialist';
OPEN Employee_Cursor;

FETCH NEXT FROM Employee_Cursor;
WHILE @@FETCH_STATUS = 0
    BEGIN
        FETCH NEXT FROM Employee_Cursor;
    END;
CLOSE Employee_Cursor;
DEALLOCATE Employee_Cursor;
GO

(Notice how FETCH NEXT FROM Employee_Cursor; appears twice.)

If the FETCH selects into a long list of variables, then we have a large duplicated statement which is both ugly and of course, "non-DRY" code.

I'm not aware of a post-condition control-of-flow T-SQL statement so it seems I'd have to resort to a WHILE(TRUE) and then BREAK when @@FETCH_STATUS is not zero. This feels clunky to me.

What other options do I have?

1
  • 2
    In code you shown, replace first FETCH NEXT FROM Employee_Cursor with GOTO Employee_Cursor_Fetch and place label Employee_Cursor_Fetch: immediately before remaining FETCH NEXT. You can notice that label name is derived from cursor name (Employee_Cursor) to save you from some thinking about label names. Commented Jun 26, 2016 at 21:05

6 Answers 6

16

There's a good structure posted online by Chris Oldwood which does it quite elegantly:

DECLARE @done bit = 0 

WHILE (@done = 0)  
BEGIN 
  -- Get the next author.  
  FETCH NEXT FROM authors_cursor  
  INTO @au_id, @au_fname, @au_lname  

  IF (@@FETCH_STATUS <> 0) 
  BEGIN 
    SET @done = 1 
    CONTINUE 
  END 

  -- 
  -- stuff done here with inner cursor elided 
  -- 
END
Sign up to request clarification or add additional context in comments.

Comments

12

This is what I've resorted to (oh the shame of it):

WHILE (1=1)
BEGIN
    FETCH NEXT FROM C1 INTO
   @foo,
   @bar,
   @bufar,
   @fubar,
   @bah,
   @fu,
   @foobar,
   @another,
   @column,
   @in,
   @the,
   @long,
   @list,
   @of,
   @variables,
   @used,
   @to,
   @retrieve,
   @all,
   @values,
   @for,
   @conversion

    IF (@@FETCH_STATUS <> 0)
    BEGIN
        BREAK
    END

     -- Use the variables here
END

CLOSE C1
DEALLOCATE C1

You can see why I posted a question. I don't like how the control of flow is hidden in an if statement when it should be in the while.

1 Comment

I think, technically, this should be the accepted answer, as it does avoid duplicating the FETCH statement.
4

The first Fetch shouldn't be a Fetch next, just a fetch.

Then you're not repeating yourself.

I'd spend more effort getting rid of the cursor, and less on DRY dogma, (but if it really matters, you could use a GOTO :) - Sorry, M. Dijkstra)

GOTO Dry
WHILE @@FETCH_STATUS = 0 
BEGIN 
    --- stuff here

Dry:
    FETCH NEXT FROM Employee_Cursor; 
END; 

2 Comments

I totally agree with the stance on cursors. Sadly, I must allow some rows to fail without failing the entire update. I had my insert-into-select all done and then I had to resort to cursors to ignore failing conversions in my select. Ho hum.
@BernhardHofmann Can you not pre-emptively catch the failures with a WHERE clause?
0

Here is my humble contribution. Single FETCH statement, no GOTO, no BREAK, no CONTINUE.

-- Sample table
DROP TABLE IF EXISTS #tblEmployee;
CREATE TABLE #tblEmployee(ID int, Title varchar(100));
INSERT INTO #tblEmployee VALUES (1, 'First One'), (2, 'Second Two'), (3, 'Third Three'), (3, '4th Four');
-- Cursor with one FETCH statement
DECLARE @bEOF bit=0, @sTitle varchar(200), @nID int;
DECLARE cur CURSOR LOCAL FOR SELECT ID, Title FROM #tblEmployee;
OPEN cur;
WHILE @bEOF=0
BEGIN
    FETCH NEXT FROM cur INTO @nID, @sTitle;
    IF @@FETCH_STATUS<>0
        SET @bEOF=1;
    ELSE
    BEGIN
        PRINT Str(@nID)+'. '+@sTitle;
    END;
END;
CLOSE cur;
DEALLOCATE cur;
-- Cleanup
DROP TABLE IF EXISTS #tblEmployee;

Comments

-2

It is obvious that a cursor is the pointer to the current row in the recordset. But mere pointing isn't gonna make sense unless it can be used. Here comes the Fetch statement into the scene. This takes data from the recordset, stores it in the variable(s) provided. so if you remove the first fetch statement the while loop won't work as there is not "FETCHED" record for manipulation, if you remove the last fetch statement, the "while" will not loop-through.

So it is necessary to have both the fetch statement to loop-through the complete recordset.

1 Comment

It's not necessary (see my answer above). I was asking for better ways to avoid writing the control of flow, not whether I need to have the fetch twice.
-5

Simply said you can't... that's just how most where statements in SQL work. You need to get the first line before the loop and then do it again in the while statement.

The better question how to get rid of the cursor and try to solve your query without it.

3 Comments

Strongly disagree. You can avoid the duplication, though removing the cursor is a worthy aim too.
While I would agree that in most cases cursors should be avoided in favor of set based operations, there are some instances where a cursor is absolutely necessary. Ie. Where rows need to be evaluated on a line by line basis and the result of previously read rows will determine the result of subsequent reads.
I know I'm late to the game, but this is first on my Google search - why is this the accepted answer? It's simply not correct, as shown multiple times already (although after this one was selected as the answer). Please select the best of the other answers :-) I have used Bernhard's solution, but Vince's is also quite OK. I can't let myself use GOTOs of sheer terror of getting mocked by other developers (even though break/continue actually is the same thing) :D

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.