views:

82

answers:

5

Hi, I have a question. I am working on cursors. Each time, after fetching the last records and printing its data’s, the cursor prints an addition line. To understand what I mean please consider the following sample example: I want to print the information about only 10 customers.

USE Northwind
GO

DECLARE myCursor CURSOR 
FOR SELECT TOP(10) ContactName FROM Customers
DECLARE @RowNo int,@ContactName nvarchar(30)
SET @RowNo=1
OPEN myCursor
FETCH NEXT FROM myCursor INTO @ContactName
PRINT  LEFT(CAST(@rowNo as varchar) + '      ',6)+'  '+ @ContactName
SET @RowNo=@RowNo+1
SET @ContactName=''
WHILE @@FETCH_STATUS=0
  BEGIN
        FETCH NEXT FROM myCursor INTO @ContactName
        PRINT + LEFT(CAST(@rowNo as varchar) + '      ',6)+'  '+ @ContactName
        SET @RowNo=@RowNo+1
        SET @ContactName=''
  END
CLOSE myCursor
DEALLOCATE myCursor

Now look at the output:

1       Maria Anders
2       Ana Trujillo
3       Antonio Moreno
4       Thomas Hardy
5       Christina Berglund
6       Hanna Moos
7       Frédérique Citeaux
8       Martín Sommer
9       Laurence Lebihan
10      Elizabeth Lincoln
11      

The row number 11 also has been printed. Is it a problem in a cursor or it always occurs? Is there any way not to print this addition data? Thanks (i use sql erver 2008)

+1  A: 

See how you have the printing logic duplicated? That's a pointer to what's going wrong. Your loop should look like this:

FETCH NEXT INTO @working_variables
WHILE @@FETCH_STATUS = 0
    -- process @working_variables
    FETCH NEXT INTO @working_variables

The only duplicated code should be the FETCH NEXT itself - the way you have it now, the last FETCH happens, but you PRINT a line before the WHILE can exit.

AakashM
+3  A: 

Either...

FETCH NEXT FROM myCursor INTO @ContactName
WHILE @@FETCH_STATUS = 0
BEGIN
    -- do stuff

    FETCH NEXT FROM myCursor INTO @ContactName
END

Or...

WHILE @@FETCH_STATUS = 0
BEGIN
    FETCH NEXT FROM myCursor INTO @ContactName
    IF @@FETCH_STATUS = 0
    BEGIN
        -- do stuff
    END
END

Or...

WHILE (1 = 1)
BEGIN
    FETCH NEXT FROM myCursor INTO @ContactName
    IF @@FETCH_STATUS <> 0
        BREAK

    -- do stuff
END
LukeH
+1, I like the `WHILE (1 = 1)` version, only have one 'fetch` and it is at the top of the loop so `CONTINUE` can be used
KM
WHILE 1=1 FETCH/BREAK is the way to go.
Peter
Yes, where have these constructions been all my life? I've always used the first form, and hated the redundancy. (Sorry, I know this comment is kind of redundant with @KM's.)
harpo
A: 

A FETCH at the end of the record set sets @@FETCH_STATUS to not 0.

The FETCH NEXT command should be the last line in the WHILE BLOCK.

USE Northwind
GO

DECLARE myCursor CURSOR 
FOR SELECT TOP(10) ContactName FROM Customers
DECLARE @RowNo int,@ContactName nvarchar(30)
SET @RowNo=0
OPEN myCursor
FETCH NEXT FROM myCursor INTO @ContactName
WHILE @@FETCH_STATUS=0
  BEGIN

        SET @RowNo=@RowNo+1
        SET @ContactName=''
        PRINT + LEFT(CAST(@rowNo as varchar) + '      ',6)+'  '+ @ContactName
        FETCH NEXT FROM myCursor INTO @ContactName
  END
CLOSE myCursor
DEALLOCATE myCursor
Darryl Peterson
A: 

This is an off-by-one error. Here's a better way to iterate through a cursor, w/ less code duplication:

USE Northwind
GO

DECLARE myCursor CURSOR 
FOR SELECT TOP(10) ContactName FROM Customers
DECLARE @RowNo int,@ContactName nvarchar(30)
SET @RowNo=0 -- initialize counters at zero, increment after the fetch/break
OPEN myCursor
WHILE 1=1 BEGIN -- start an infinite loop
  FETCH NEXT FROM myCursor INTO @ContactName
  IF @@FETCH_STATUS <> 0 BREAK
  SET @RowNo=@RowNo+1
  PRINT  LEFT(CAST(@rowNo as varchar) + '      ',6)+'  '+ @ContactName
END
CLOSE myCursor
DEALLOCATE myCursor

For extra points, use a cursor variable and declare w/ FAST_FORWARD and TYPE_WARNING, or STATIC for small datasets. eg:

DECLARE @cursor CURSOR
SET @cursor = CURSOR FAST_FORWARD TYPE_WARNING FOR
  SELECT TOP (10) ContactName FROM Customers
OPEN @cursor 
......
CLOSE @cursor
DEALLOCATE @cursor

CLOSE and DEALLOCATE are not strictly necessary, as the cursor variable will go out of scope at the end of the batch. It is still good form, however, as you might add more code at the end later on, and you should free up resources as early as possible.

TYPE_WARNING tells you when SQL Server implicitly converts the requested cursor type (FAST_FORWARD) to another type (typically STATIC), if the requested type is incompatible w/ your SELECT statement.

Peter
LOL downvote w/out explanation. Cursor hate? If there is a technical error somewhere, please tell.
Peter
@Peter, looks like all 4ish answers given before a particular time got a drive-by downvote. Here, have a +1.
AakashM
+2  A: 

You mentioned you're using SQL Server 2008. With SQL Server 2005 or greater, you don't need a cursor at all to do what you want.

select top 10 left(cast(row_number() over(order by ContactName) as varchar)+ '      ', 6) + ContactName
    from Customers
Joe Stefanelli
Thank you, I was starting to wonder why everyone was telling her how to fix the cursor and not why she shouldn't use it at all!For reasons why not to use cursors and how to avoid them: http://wiki.lessthandot.com/index.php/Cursors_and_How_to_Avoid_Them
HLGEM
I was wondering the same thing! :-)
Joe Stefanelli
@HLGEM, the example may have been trimmed down to better illustrate the problem. In any case, cursors have their place. The SQL Server community dogmatically shuns them to the point of ignorance.
Peter
@Peter, we shun them because they usually perform poorly and because they are almost always more complicated to code than the set-based logic that you can use instead. It is silly to learn to use a poorly performing technique when a better one is available and it involves simpler code as well. Nothing she said needs a cursor. If she is doing something more complicated, then she could have said so.
HLGEM
@HLGEM: show me a construct that is more convenient or efficient for non-set iteration than a cursor. eg: you need to call procedure x once for each row in set y, grab output parameter values, and log exceptions. What do you use as a looping construct? Create a table variable w/ an identity column and loop over that? This is no better than a static cursor, and more long-winded. All you have preserved is set-based purity. They have their place. Ignorance makes their use inefficient.
Peter
Well except for system procs, I would never consider calling procs one at a time for a set of values, I write set-based procs not individual row procs and now that you can use table variables as input variables there is no loger an excuse to write a proc that inserts one record.
HLGEM