Search Suggest

SQL Server: Cursors explained in a simple way. A trick before Christmas: how to make Cursors work faster!

 Hi guys,

Welcome back mates and happy sunday!

Today an interesting post awaits us.

Christmas is coming and so today free tricks for everyone!
The trick of today is how to make cursors work faster.
But first we will see what cursors are (i know, there are a couple of posts that I don't talk about theory and I would not want you to lose the habit ... haha)
 
A personal premise: cursors are not known to be fast and I honestly don't find situations where it is preferable to use them.

This is because I have this important concept in mind

If we really want to help SQL Server execute queries, it is very important to pay attention to using "set-based logic" rather than single-line logic

Why updating a table by making an update for each row when making a single one involving all the rows is certainly faster?

But first let me give you a little introduction to the cursors.  
 
Enjoy the post mates ...enjoy it!


What is a cursor

What is a cursor? 

With over 20 years of experience I can't explain this concept it's an easier way:

A cursor is simply an object che permit to read data row by row from a given query.
My query data will be read row by row via a while loop. 
 
T-SQL has its own syntax for declaring and using cursors which we will see right away


The cursor syntax

 
We have a query, for example the following query:

Select id,code from orderdemo

And this Query return some data:

We declare a cursor with with syntax:


DECLARE cursor_name CURSOR
FOR select_statement;

Instead of "select_statement" we write our T-SQL select statement:


DECLARE cursor_name CURSOR
FOR
Select id,code from orderdemo;

 
Now we need to open the cursor (..and then open our query) and retrieve the data from it.

To open the cursor just execute the open command:

OPEN cursor_name;


 To retrieve (fetch) the first row of the query just execute the command:

FETCH NEXT FROM
cursor_name INTO variable_list;

The values ​​read by the query will be stored inside variables.
 
In our example we have a query that has two fields (id and code) so we will define two variables:

DECLARE @id Integer;
DECLARE @code VarChar(20)


We will put these two declarations before the declare of the cursor.
 
Important:
  • A variable must be declared for each field of the query
  • for clarity I have given the variable the same name as the field
 
Now that we have read the first row from our query and we have put the values ​​inside the variables we need to read all the other rows
We do this via a while loop:
WHILE @@FETCH_STATUS = 0  
BEGIN

-- instruction to be executed

FETCH NEXT FROM cursor_name;
END;


The global function @@ FETCH_STATUS returns a zero value when the FETCH command is successful. In this case we have successfully read a row from the query and have not reached the end of the data to read.
 
The FETCH NEXT FROM command read the next row of data.

While we are inside this WHILE cycle we can execute whatever T-SQL instruction.

After we have read all the data we need to close and then destroy the cursor:

CLOSE cursor_name;
DEALLOCATE
cursor_name;


Let's put it all together now... This is our first cursor!
 
DECLARE @id Integer;
DECLARE @code VarChar(20)
DECLARE cursor_name CURSOR
FOR Select id,code from orderdemo;

OPEN cursor_name;
FETCH NEXT FROM cursor_name INTO @id, @code
WHILE @@FETCH_STATUS = 0
BEGIN

-- instruction to be executed

FETCH NEXT FROM cursor_name;
END;
CLOSE cursor_name;
DEALLOCATE cursor_name;


Instead of instruction to be executed we can write any query. 

For example:

 
DECLARE @id Integer;
DECLARE @code VarChar(20)
DECLARE cursor_name CURSOR
FOR Select id,code from orderdemo;

OPEN cursor_name;
FETCH NEXT FROM cursor_name INTO @id, @code
WHILE @@FETCH_STATUS = 0
BEGIN

UPDATE orderdemo SET code = (CASE WHEN code = '01' THEN 'AA' ELSE 'BB' END) WHERE id = @id

FETCH NEXT FROM cursor_name;
END;
CLOSE cursor_name;
DEALLOCATE cursor_name;


Just select all the code and execute it:

This is the result:


From the point of view of their use now you know everything!

I remember that from the point of view of performance, performing "n" updates is less fast than performing a single massive update on all row.

But now it's time for the trick as promised


How to speed up a cursor

Often those who use cursors find themselves in the situation that its execution is very slow!
 
Let's see what happens with an example.
 
We have a table that contain prices of various products:

CREATE TABLE [dbo].[Prices](
[id] [int] identity(1,1) NOT NULL,
[idProd] [int] NULL,
[Price] [float] NULL
) ON [PRIMARY]

We have defined a clustered index on the column ID and a non-clustered index on the IdProd column.

CREATE CLUSTERED INDEX CI_Prices_Id ON Prices([id] ASC)
CREATE INDEX IDX_Prices_IdPRod ON Prices(IdProd)

The table looks like so:




Suppose now to run this query:

SELECT id, IdProd FROM Prices WHERE idprod = 1

From the execution plan we see that corretcly the non clustered index IDX_Prices_IdProd is used:
 

Now let's put this same query in our cursor ..
 
DECLARE @id Integer;
DECLARE @idprod Integer;
DECLARE cursor_name CURSOR
FOR Select id,idprod from Prices where IdProd = 1

OPEN cursor_name;
FETCH NEXT FROM cursor_name INTO @id,@idprod
WHILE @@FETCH_STATUS = 0
BEGIN

-- instruction to be executed

FETCH NEXT FROM cursor_name INTO @id,@idprod
END;
CLOSE cursor_name;
DEALLOCATE cursor_name;


And take a look to the execution plan again.
 
Hey! this time in the execution plan we have an additional object, a Key Lookup.

SQL Server uses the non clustered index con the IdProd column but then has to access again to the table to get the Id column via the clustered index.



From the performances point of view the cost of the non clustered index is equal to 0.004 while the cost of the Key lookup is equal to 1,65: cost thousands of times higher!

Here is the reason for a cursor that takes so long to run.

So, how to solve the problem?

Just add to the cursor the FAST_FORWARD option:

 
DECLARE @id Integer;
DECLARE @idprod Integer;
DECLARE cursor_name CURSOR FAST_FORWARD
FOR Select id,idprod from Prices where IdProd = 1

OPEN cursor_name;
FETCH NEXT FROM cursor_name INTO @id,@idprod
WHILE @@FETCH_STATUS = 0
BEGIN

-- instruction to be executed

FETCH NEXT FROM cursor_name INTO @id,@idprod
END;
CLOSE cursor_name;
DEALLOCATE cursor_name;


Let's run again...
 
This time, with the FAST_FORWARD option, the lookup is disappeared!
 

Goood very very good mates!

If we compare the execution times of both the versions of our cursor we have 33 seconds without the FAST_FORWARD option and only 12 seconds if we add it.




Well it seems like a great trick! 
 
Do you want to know exactly what happens?
You just have to follow me also in the next posts!
 

That's all for today!
I wish you a great week ahead and a happy sunday.












Next post:

Post a Comment