Курсори

В предната статия се натъкнахме на често срещан проблем. Имахме да извършим множество подобни операции за всички обекти от дадена таблица. Възползвахме се от възможността да ползваме цикъл, но как да итерираме през оригиналната таблица, използвайки броят на редовете и за край на цикъла и итератор, движещ се по id. Има възможност много от записите в тази таблица да са вече изтрити. Съответно и id-тата им също са били изтрити. Ще имаме много празни итерации. Друг вариант е да правим селект от таблицата и накрая на заявката да ограничаваме до LIMIT 1 OFFSET iterator и на всяка итерация да инкрементираме iterator. Това е добра стратегия, но не е съвсем традиционна. Има специални обекти, чрез които можем да итерираме през всякакви селект заявки, дори и през многотаблични заявки.  Наричат се курсори.

Курсорите дават възможност да итерираме върху множество от редове, върнати в резултат от SELECT заявка и да обработваме всеки един ред по отделно. Курсорите в MySQL са: read-only, non-scrollable, asensitive.

  • Read-only: Използват се само за четене на ред от селект заявка. Чрез тях не можете да модифицирате информация от таблиците, от които тя се взима.
  • Non-scrollable: Курсорите не позволяват прескачане или пропускане на редове. Единствената възможност, която предлагат, е последователно изваждане на следващ ред от резултата от SELECT заявката, за която са създадени.
  • Asensitive: asensitive и insensitive курсорите се различават по актуалността на данните, от които четат. Sensitive- курсорите четат информация директно от оригиналните таблици, участващи в SELECT заявките, а insensitive – курсорите четат информация от временно копие на тези таблици.Asensitive са по-бързи, тъй като не отнемат време от създаване на временно копие на оригиналните данни, но пък ако някой модифицира данните в същото време, то курсорът ще чете модифицираните данни.  Някои СУБД предлагат и двата вида курсори. MySQL-курсорът е asensitive. Можете да ги използвате в процедури, в тригери и във функции.

Синтаксис:

DECLARE <име на курсора> CURSOR  FOR<заявка от тип SELECT>;

Запомнете, че трябва да декларирате винаги курсорът след  всички декларации на променливи. Тоест – след курсора не трябва да има повече декларирани променливи. Когато решите да започнете работа с курсора, ще трябва да го отворите. Става чрез командата:

OPEN  <име на курсора>;

Преди курсора е редно да сте си декларирали променливи, в които да записвате временните стойности на всяка колона от селект заявката на курсора. След това, когато решите да изваждате редове от курсора и стойността на всяка колона да записвате в променлива, трябва да използвате команда:

FETCH <име на курсора> INTO <списък с променливи>;

Извършвате нужните действия с променливите и накрая го затваряте чрез:

CLOSE <име на курсора>;

Възможни грешки и начин за прихващане и обработка:

В миналата статия споменахме, че е добре да прихващате грешка от типа NOT FOUND – в случай, че курсорът е достигнал края на множеството, върнато от селект-а.

DECLARE CONTINUE HANDLER FOR NOT FOUND  SET finished = 1;

С променливата finished означаваме, че курсорът е достигнал края, а самата декларация на handler-а трябва да бъде точно след декларацията на променливите и на курсора, за да важи от там надолу.

Нека да разгледаме следния пример. Ще направим курсор, който взима всички треньори с ненулева месечна заплата и ги разпечатва един по един на екрана. Без да ползва временна таблица, на всяка итерация на while цикъла, ще се измъква по един ред от курсова и ще се присвоява на две временни променливи. Когато в курсора няма нищо, то се генерира грешка NOT FOUND и handler-а, дефиниран по-горе променя стойността на finished на 1. Проверка дали finished не е станало 1 се прави вътре в цикъла, защото за да се генерира грешка поради край на курсор, то ние трябва да сме вече в цикъла. Ако това е така, то напускаме текущия цикъл с команда LEAVE, последвана от името на етикета, което сме избрали. След като излезем от цикъла затваряме отворения курсор.

 

#курсори
use school_sport_clubs;
drop procedure if exists  CursorTest;
delimiter |
create procedure CursorTest()
begin
declare finished int;
declare tempName varchar(100);
declare tempEgn varchar(10);
declare coachCursor CURSOR for
SELECT name, egn
from coaches
where month_salary is not null;
declare continue handler FOR NOT FOUND set finished = 1;
set finished = 0;
OPEN coachCursor;
coach_loop: while( finished = 0)
DO
FETCH coachCursor INTO tempName,tempEgn;
       IF(finished = 1)
       THEN 
       LEAVE coach_loop;
       END IF;	
       SELECT tempName,tempEgn; # or do something with these variables...
end while;
CLOSE coachCursor;
SET finished = 0;
SELECT 'Finished!';
end;
|
delimiter |

Можете да извикате процедурата и ще видите, че тя ги разпечатва един по един.

Нека да преработим втората от двете процедури в предната статия – тази, която смята хонорарите и ги добавя към месечните заплати на треньорите:

 

use school_sport_clubs;

drop procedure if exists OPTIMIZED_monthHonorariumPayment;

delimiter |
create procedure OPTIMIZED_monthHonorariumPayment(IN monthOfPayment INT, in yearOFpayment INT)
procLabel: begin
declare countOfCoaches int;
declare iterator int;
declare countOfRowsBeforeUpdate int;
declare countOfRowsAfterUpdate int;
declare finished int;
declare tempCoachId int;
declare tempSumOfHours int;

 DECLARE tempCoachCursor CURSOR FOR
    SELECT  coach_id, SUM(number_of_hours)
	FROM coach_work
	where month(coach_work.date) = monthOfPayment
	AND YEAR(coach_work.date) = yearOFpayment
	AND isPayed = 0
	GROUP BY coach_work.coach_id;
    
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SELECT 'SQL Exception';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;

create temporary table tempTbl(
id int auto_increment primary key,
coach_id int,
number_of_hours int,
pay_for_hour decimal,
amount decimal,
paymentMonth int,
paymentYear int
)Engine = Memory;
	
	#Плащане на редовна месечна заплата:
    SET @RESULT =0;
    call monthSalaryPayment(monthOfPayment, yearOFpayment, @RESULT);
    SELECT @RESULT as resultFromMonhtPayment; #only for control and test
    
   	SELECT COUNT(*)
    INTO countOfRowsBeforeUpdate
    FROM coach_work
    where month(coach_work.date) = monthOfPayment
    AND YEAR(coach_work.date) = yearOFpayment
    AND isPayed = 0;
    
    START TRANSACTION;
    OPEN tempCoachCursor;
    set finished = 0;
while_loop_label: WHILE(finished = 0)
	DO
	FETCH tempCoachCursor INTO  tempCoachId, tempSumOfHours;
		
   IF(finished = 1)
      THEN leave while_loop_label;
      ELSE
	SELECT  tempCoachId, tempSumOfHours;
	INSERT INTO tempTbl(coach_id, number_of_hours, pay_for_hour, amount, paymentMonth, paymentYear)
	SELECT tempCoachId, tempSumOfHours, c.hour_salary, tempSumOfHours*c.hour_salary, monthOfPayment, yearOFpayment
        FROM coaches as c
	WHERE c.id = tempCoachId;
	END IF;
	END WHILE;
    CLOSE tempCoachCursor;
    
    SELECT * FROM tempTbl;#only for control and test
	INSERT INTO salarypayments(`coach_id`, `month`,`year`,`salaryAmount`,`dateOfPayment`)
        SELECT coach_id, paymentMonth, paymentYear, amount, NOW()
	FROM tempTbl
        ON DUPLICATE KEY UPDATE 
        salaryAmount = salaryAmount + amount,
        dateOfPayment = NOW();

	UPDATE coach_work
	SET isPayed = 1
	WHERE month(coach_work.date) = monthOfPayment
	AND YEAR(coach_work.date) = yearOFpayment
	AND isPayed = 0;
	SELECT  ROW_COUNT() INTO countOfRowsAfterUpdate;
    SELECT countOfRowsAfterUpdate as countOfRowsAfterUpdate; #only for control and test
    SELECT countOfRowsBeforeUpdate as countOfRowsBeforeUpdate;#only for control and test
	  IF(countOfRowsBeforeUpdate = countOfRowsAfterUpdate)
      THEN 
		commit;
      ELSE
		rollback;
      END IF;
	drop table tempTbl;
END;
|
delimiter ;

Ваша задача е да оптимизирате и процедурата, която извършва плащане на основни месечни заплати като добавите курсори. Тествайте с различни данни и помислете как да махнем и временната таблица, която остава в тази процедура.

Даниел Джолев

Leave a Reply

Your email address will not be published. Required fields are marked *