Nesting MySQL Cursor Loops
Handling cursor loops in MySQL keeps puzzling people.
Single Cursor Loops
The common case is to have a simple cursor loop. Each record is fetched from the cursor until there are now more records, and for each record some work is performed.
Let's take a look at this snippet:
01 begin
02 declare v_col1 int; -- define the data to grab from the cursor
03 declare no_more_rows boolean := FALSE; -- define the loop control variable
04 declare cursor1 cursor for -- define the set to iterate through
05 select col1
06 from MyTable;
07 declare continue handler for not found -- Capture cursor exhaustion event
08 set no_more_rows := TRUE; -- modify the loop control variable, then return
09
10 open cursor1; -- execute query, and save the result so we can iterate it
11 LOOP1: loop -- start repeatable region of code
12 fetch cursor1 -- grab data from current cursor record
13 into v_col1 -- this will raise NOT FOUND if the cursor is exhausted
14 ;
15 if no_more_rows then -- check the loop control variable, the handler might've modified it
16 close cursor1; -- free resources
17 leave LOOP1; -- stop iterations
18 end if;
19 --
20 -- Process data here
21 --
22 end loop LOOP1;
23 end;
This is a fragment of a MySPL Procedure.
To summarize, the
CURSOR is processed by iteration through an ordinary LOOP that happens to FETCH rows from the cursor. When the cursor's exhausted, the NOT FOUND condition is raised, transferring control to a HANDLER that sets a flag. This flag is checked inside the loop, right after the fetch, and when it is set, iteration is terminated.Handling cursor exhaustion
An important point to make right away is that, unlike the
OPEN (line 10), FETCH (line 12) and CLOSE (line 16) statements, there is no formal relationship between the HANDLER declaration (lines 07..08) and the CURSOR declaration (lines 04..06). For instance, the HANDLER declaration never refers to cursor1, the name of the cursor. It just 'happens' to be the case that FETCH-ing eventually leads to a situation in wich the cursor will have reached it's last record. Such an exhausted cursor will raise the NOT FOUND condition on attempt to FETCH from it again. This condition can then be captured by the, scopewise, nearest appropriate HANDLER.Nested Cursor Loops
Sometimes you need to nest two cursors. I don't want to discuss what problems are appropriately solved with this device right now. Let me just say that most of the time someone is using this device, they are doing that because they don't know how to write a join properly.
The problem with nesting curors is this: because we can't associate a
HANDLER with a particular CURSOR we have to think of a way to know wich one of the cursors is exhausted once the NOT FOUND condition is raised. We really need to know that, because our loop controls depend on it. We definitely want to terminate the corrsponding loop when one of the cursors will be exhausted, don't we?Using a separate block
Perhaps one of cleanest ways to do this is to rely on the scopewise propagation of the condition. By putting the inner
LOOP into it's own BEGIN..END block, we have created the possibility to give that block it's own NOT FOUND handler. A NOT FOUND condition raised by a FETCH in the inner LOOP will thus be captured by the appropriate HANDLER declared in the inner BEGIN..END block. That's because that inner block's handler is nearer than the handler declared in the outer block. From there, the inner loop can easily be terminated:
01 BLOCK1: begin
02 declare v_col1 int;
03 declare no_more_rows boolean1 := FALSE;
04 declare cursor1 cursor for
05 select col1
06 from MyTable;
07 declare continue handler for not found
08 set no_more_rows1 := TRUE;
09 open cursor1;
10 LOOP1: loop
11 fetch cursor1
12 into v_col1;
13 if no_more_rows1 then
14 close cursor1;
15 leave LOOP1;
16 end if;
17 end loop LOOP1;
18 BLOCK2: begin
19 declare v_col2 int;
20 declare no_more_rows2 boolean := FALSE;
21 declare cursor2 cursor for
22 select col2
23 from MyOtherTable
24 where ref_id = v_col1;
25 declare continue handler for not found
26 set no_more_rows2 := TRUE;
27 open cursor2;
28 LOOP2: loop
29 fetch cursor2
30 into v_col2;
31 if no_more_rows then
32 close cursor2;
33 leave LOOP2;
34 end if;
35 end loop LOOP2;
36 end BLOCK2;
37 end BLOCK1;
Note that the inner
BEGIN..END block's handler can never capture the NOT FOUND condition raised by the FETCH in the outer block, because conditions are propagated from the inside to the outside. Another advantage of this approach is that the inner loop is almost entirely self-contained. The only thing that binds the inner
BEGIN..END block to the outer one is a reference to the variable v_col1 in the decalaration of cursor2. Everything that has to do with the inner loop is defined in just one place, BLOCK2.Resetting the loop control variable
The disadvantage of the previous method is of course the duplication of code. Apart from the cursor itself, we have to declare the loop control variable and the handler twice too. Sometimes, this is way too much trouble. Lucky for us, there's a way to evade all that, by resetting the loop control variable just before the inner loop terminates. Check it out:
00 begin
01 declare no_more_rows boolean default false;
02 declare v_col1 int;
03 declare v_col2 int;
04 declare cursor1 cursor for
05 select col1
06 from MyTable
07 ;
08 declare cursor2 cursor for
09 select col2
10 from MyOtherTable
11 where ref_id = v_col1;
12 declare continue handler for not found
13 set v_done := true;
14
15 open cursor1;
16 LOOP1: loop
17 fetch cursor1 into v_col1;
18 if v_done then
19 close cursor1;
20 leave LOOP1;
21 end if;
22 open cursor2;
23 LOOP2: loop
24 fetch cursor2 into v_col2;
25 if v_done then
26 set v_done := false;
27 close cursor2;
28 leave LOOP2;
29 end if;
30 end loop LOOP2;
31 end loop LOOP1;
32 end;
Observe te difference between the handling of the loop termination. For the outer loop, we see the usual sequence of statements to close the cursor and to leave the loop (lines 18..21). For the inner loop, we use an additional assignment to reset the loop control variable (line 26). This ensures the outer loop gets a chance to roll off untill it's exhausted too.
Packing cursor loops into stored procedures
Another approach would be to pack all the code for the dependant, inner cursor loop into a separate stored procedure and have the outer loop
CALL that. This approach resembles the first one where we put the inner loop in it's own BEGIN..END block:
01 create procedure p_inner_loop(
02 IN p_col1 int
03 )
04 begin
05 declare v_col int;
06 declare no_more_rows boolean := FALSE;
07 declare cursor cursor1 for
08 select col2
09 from MyOtherTable
10 where ref_id = p_col1;
11 declare continue handler for not found
12 set no_more_rows := TRUE;
13 open cursor1;
14 LOOP1: loop
15 fetch cursor1
16 into v_col;
17 if no_more_rows then
18 close cursor1;
19 leave LOOP1;
20 end if;
21 end loop LOOP1;
22 end;
And
01 BLOCK1: begin
02 declare v_col1 int;
03 declare no_more_rows boolean1 := FALSE;
04 declare cursor1 cursor for
05 select col1
06 from MyTable;
07 declare continue handler for not found
08 set no_more_rows1 := TRUE;
09 open cursor1;
10 LOOP1: loop
11 fetch cursor1
12 into v_col1;
13 if no_more_rows1 then
14 close cursor1;
15 leave LOOP1;
16 end if;
17 call p_inner_loop(v_col1);
18 end loop LOOP1;
19 end BLOCK1;
This approach is especially useful when you want to be able to use that inner loop from other contexts as well. When cursor processing is becoming complex, it's probably a good idea to use this technique as a "divide and conquer" strategy to keep development managable. For example, this technique allows you test the different cursor loops separate from each other, wich can be a great aid in debugging.


28 Comments:
I found it very useful, as compared to mysql online documentation.
Milind Paralkar
By
Anonymous, at 12:34 PM
Hi Milind,
that's great, I'm so glad it helped.
Cursor loops, and nesting them still seems to be misunderstood by quite some people. I'm just glad this little article helped you using them.
kind regards,
Roland
By
Roland Bouman, at 3:10 AM
declare no_more_rows boolean1 := FALSE;
should be no_more_row1 boolean := FALSE;?
-K
By
Anonymous, at 3:53 PM
Yes -K (?)
you are absolutely right. Thanks for the spot.
Anyone wondering were the hack this comes from, check out the second listing in the blog. There, the declaration should indeed be in accordance with the correction.
Thanks again!
By
Roland Bouman, at 4:01 PM
for me the cursor only work when a put set v_done := false in all loops
OPEN cxReport; -- Open cursor
LOOP2: loop -- do until
fetch cxReport into vReportName; -- read record
if v_done then -- if found end of cursor
set v_done := false; -- set found as false
close cxReport; -- close cursor
leave LOOP2; -- leave loop
end if;
OPEN cxEnv;
LOOP3: loop
fetch cxEnv into vEnvName;
if v_done then
set v_done := false;
close cxEnv;
leave LOOP3;
end if;
SET vCount = 0;
OPEN cxBill;
LOOP4:loop
fetch cxBill into vBill;
if v_done then
set v_done := false;
close cxBill;
leave LOOP4;
end if;
SET vLineDuplicate = 0;
-- check bill period and build the correct start and end date for cbss
IF (MONTH(vDate) = 1 AND vBill = 28) THEN
SELECT CONCAT(YEAR(vDate)-1,'-','12','-',vBill,' 00:00:00') INTO vBillPeriod;
ELSEIF (MONTH(vDate) = 1 AND vBill <> 28) THEN
SELECT CONCAT(YEAR(vDate),'-',MONTH(vDate),'-',vBill,' 00:00:00') INTO vBillPeriod;
ELSEIF (MONTH(vDate) <> 1 AND vBill = 28) THEN
SELECT CONCAT(YEAR(vDate),'-',MONTH(vDate)-1,'-',vBill,' 00:00:00') INTO vBillPeriod;
ELSE
SELECT CONCAT(YEAR(vDate),'-',MONTH(vDate),'-',vBill,' 00:00:00') INTO vBillPeriod;
END IF;
SELECT CONCAT("CBSOI",vReportName) INTO vReportID;
SET vCount = vCount + 1;
Insert Into xRet(line1,line2,line3,line4)
Select vReportID,vBillPeriod,vEnvName,vCount;
-- Select records to see if there are duplicate recors
SELECT Count(*) as LineDuplicate INTO vLineDuplicate
FROM cbss_psp_total
WHERE ReportID = vReportID
AND BillingDate = vBillPeriod
AND DistCode = vEnvName
GROUP BY ReportID, ReportName, TaxType, Ref, ProcessingDateTime, ReportType, DistCode, DistDesc, BillingDate, State, PSP, TaxLevel, Location, Description, TaxRate, CurrentBilling, ExemptRevenue, TaxableRevenue, TotalTaxesBilled, AdjustmentTax, WORecoveryTax, NetTax
HAVING LineDuplicate > 1
LIMIT 1;
-- SELECT IFNULL(vLineDuplicate,0) INTO vLineDuplicate;
-- if has duplicate records
IF vLineDuplicate > 1 THEN
select "entre";
-- Insert the duplicate records into tmp_duplicate_records_report table
INSERT INTO tmp_duplicate_records_report(System, DuplicateAmount,DuplicateTimes,Impact,LineInfo,Id_User,UUID,DeletedTime)
SELECT vReportID,CurrentBilling,Count(*) as LineDuplicate,(CurrentBilling * (Count(*)-1)),CONCAT_WS(' | ',ReportID, ReportName, TaxType, Ref, ProcessingDateTime, ReportType, DistCode, DistDesc, BillingDate, State, PSP, TaxLevel, Location, Description, TaxRate, CurrentBilling, ExemptRevenue, TaxableRevenue, TotalTaxesBilled, AdjustmentTax, WORecoveryTax, NetTax),vId_User,vUUID, NOW()
FROM cbss_psp_total
WHERE ReportID = vReportID
AND BillingDate = vBillPeriod
AND DistCode = vEnvName
GROUP BY ReportID, ReportName, TaxType, Ref, ProcessingDateTime, ReportType, DistCode, DistDesc, BillingDate, State, PSP, TaxLevel, Location, Description, TaxRate, CurrentBilling, ExemptRevenue, TaxableRevenue, TotalTaxesBilled, AdjustmentTax, WORecoveryTax, NetTax
HAVING LineDuplicate > 1
LIMIT 1;
-- update records duplicated in tmp_checklist table changing the Image System Index
UPDATE tmp_checklist SET Status = 'DUPLICATE',
IdImage = vDUPLICATE
WHERE System = 'CBSS' And Environment = vEnvName And ReportID = vReportName
And BillPeriod = vBill And Month = MONTH(vBillPeriod) And Year = YEAR(vBillPeriod)
and Id_User = vId_User and UUID = vUUID and tablename = vTableName;
END IF;
end loop LOOP4;
end loop LOOP3;
end loop LOOP2;
but I still have problem with the 3 loop it fetch only the first record what can I do to solve the problem?
By
Odimar, at 10:03 PM
Hi Odimar,
you say that it only works for you if you reset the loop variable v_done for *ALL* loops.
If your LOOP2 really is the outmost loop, it shouldn't matter whether you reset the loop control variable or not. Either way, once the leave LOOP2 statement is executed, control is transferred to the point beyond the end LOOP2 statement. Assuming v_done is not used after that, it doesn't make a difference whether you reset it or leave it as it is.
However, your comment did make me realize that it is indeed OK to reset it also for outmost loop; this means all loops can be set up identically, which I like. Thanks for the insight!
As for your remaining problem...Ouch, difficult to say. Usually, problems are caused by an orphan SELECT or SELECT..INTO statement inside the loop. You see, if such a statement does not return any rows, the NOT FOUND condition is raised too, unintentionally firing the handler.
In your particular case, I can see one statement that could be causing this:
SELECT Count(*) as LineDuplicate
INTO vLineDuplicate
FROM cbss_psp_total
WHERE ...
GROUP BY ...
HAVING ...
LIMIT 1;
Normally, COUNT(*) queries always return a row, but in your case, the HAVING condition can be responsible for filtering out rows.
So, you could try and see of your problem persists if you remove the HAVING clause.
I hope that helps, good luck!
By
Roland Bouman, at 1:27 AM
Really a wonderful article. It helped me lot.
By
vaibogam, at 5:17 AM
Really a wonderful article. It helped me lot. Keep posting... Thanks.
By
vaibogam, at 5:20 AM
By Trying the Demo given its again showing syntax error.
By
Anonymous, at 2:57 PM
Hi Anonymous,
what demo, what syntax error? Me no understandie...
By
Roland Bouman, at 3:05 PM
Excellent guys. Keep it going.
Thanks and Regards,
Ganesh L
By
Anonymous, at 7:16 AM
this is a very helpful article. thanks for sharing your knowledge. ^_^
By
Anonymous, at 10:48 AM
Thank you for great tutorial. It's helped me a lot. I couln't find anything good on mysql site. You are the best!!!!!
By
Anonymous, at 6:09 PM
Thanks all for you kind comments.
Please do note that I think nesting cursors is a practice that really should be avoided at any cost.
In most of the cases the nested loops can be successfully refactored into a simple join. Please google my blog on "Refactoring Cursors" for a hint into this direction.
best regards,
Roland Bouman
By
Roland Bouman, at 7:49 PM
Your the fucking man! I wish more MySql tutorials were like yours.
By
Chris, at 11:27 PM
Hi Chris,
glad you liked the tutorial, thanks for your comments.
Having said that - please refrain from using the f-word, it might offend the other readers - TIA.
If you are looking for more tutorials on cursors, just use the "Search Blog" feature on this blog to locate more articles about cursors or MySQL stored procedures.
Kind regards,
Roland.
By
Roland Bouman, at 1:31 AM
This page has been INVALUABLE. It covered all the topics I was looking for, and explained everything extremely well. Thanks a million.
By
Puraz, at 5:24 AM
Thank you. It was very helpful to me.
Unfortunately mysql documentation is poor.
Great examples.
By
Alex, at 1:20 AM
Awesome article Roland, solved my problem with just a SET statement!!
BTW, could you elaborate a little bit more on how to avoid cursors? what one can do to traverse all records of a table without using cursors?
In my case, I use a cursor to read a temporary table while I update some rows. I've readed in MYSQL help that is not wise to use a cursor and update the same table that cursor points to, I don't know the implicacies of that technique but I followed their advice and used a temporary table.
Thanks again for you help!
By
Ricardo Uhalde, at 12:08 AM
Hi Roland.
I was searching a tutorial/article/faq/any-kind-of-explanation to this.
I'm a brazilian developer and started with PL/SQL recently.
Thank you.
By
Perdigão, at 6:10 PM
sweeeeeet!!!
thx a lot roland!
By
Zeft, at 8:52 PM
Hi Roland,
Thanks for the article, it was just the thing I was looking for. Helped a lot.
cheers
By
Anonymous, at 5:36 AM
I have this stored procedure
CREATE PROCEDURE `spCar`()
begin
declare exit1 int default 0;
declare rec1 char(50);
declare rec2 char(50);
declare vgas int;
declare cur1 cursor for select distinct car,driver from dates;
declare exit handler for not found set exit1=1;
open cur1;
repeat
fetch cur1 into rec2,rec1;
-- select gas into vgas from someTable where car=rec2 and driver=rec1;
select rec2,rec1,c_rest;
until exit1=1 end repeat;
close cur1;
end
if I uncommnet the line above ... the sp will not show anything if at some point the vgas variable is null
if I leave it commented will display the right number of records but I need my sp with vgas variable ... and this variable might be null at some point
By
Anonymous, at 10:54 AM
Hi anonymous, the problem is most likely that the NOT FOUND condition is triggered due to your select into fetching no rows.
You should actually wonder why you need that statement there at all - why are't you simply joining that? (Read my refactoring mysql cursors article.)
There are other problems with your approach to - the REPEAT construct is the worst possible device for a cursor loop for many reasons.
I have an article about that on this very blog too. Read it I am sure you will get an idea on how to rewrite this.
Let me know if you still have trouble with that.
kind regards,
Roland
By
Roland Bouman, at 12:18 PM
Hi Roland
I know this is an old blog but I just found it - I am an Oracle developer teaching myself MySQL, and just making my first foray into stored procedures and cursors in MySQL. Contrary to the first poster, I found your detailed analysis of the 3 looping options quite useful. So - thank you !
By
Wisdom Hunter, at 9:10 PM
EXCELLENT!!!
EXCELLENT!!!EXCELLENT!!!
EXCELLENT!!!EXCELLENT!!!EXCELLENT!!!
Thank you Roland!
By
Anonymous, at 9:20 PM
hai,
how to get the cursor records count ?
like oracle statement..
cursorName.count
like the above, is it possible to find the cursor records count in MySQL?
Thanks,
By
Kannan, at 2:50 PM
Hi Kannan,
No, MySQL does no support cursor attributes. Of course, it's a slight effort to add a local variable to do the counting yourself.
By
Roland Bouman, at 3:16 PM
Post a Comment
Links to this post:
Create a Link
<< Home