
什么是游標?
在數(shù)據(jù)庫中,游標是個重要的概念,它提供了一種靈活的操作方式,可以讓我們從數(shù)據(jù)結(jié)果集中每次提取一條數(shù)據(jù)記錄進行操作。游標讓 SQL 這種面向集合的語言有了面向過程開發(fā)的能力??梢哉f,游標是面向過程的編程方式,這與面向集合的編程方式有所不同。
在 SQL 中,游標是一種臨時的數(shù)據(jù)庫對象,可以指向存儲在數(shù)據(jù)庫表中的數(shù)據(jù)行指針。這里游標充當了指針的作用,我們可以通過操作游標來對數(shù)據(jù)行進行操作。
比如我們查詢了 heros 數(shù)據(jù)表中最大生命值大于 8500 的英雄都有哪些:
SELECT id, name, hp_max FROM heros WHERE hp_max > 8500;

如何使用游標?
游標實際上是一種控制數(shù)據(jù)集的更加靈活的處理方式。
如果我們想要使用游標,一般需要經(jīng)歷五個步驟。不同 DBMS 中,使用游標的語法可能略有不同。
第一步,定義游標。
DECLARE cursor_name CURSOR FOR select_statement
這個語法適用于 MySQL,SQL Server,DB2 和 MariaDB。如果是用 Oracle 或者 PostgreSQL,需要寫成:
DECLARE cursor_name CURSOR IS select_statement
要使用 SELECT 語句來獲取數(shù)據(jù)結(jié)果集,而此時還沒有開始遍歷數(shù)據(jù),這里 select_statement 代表的是 SELECT 語句。
第二步,打開游標。
OPEN cursor_name
當我們定義好游標之后,如果想要使用游標,必須先打開游標。打開游標的時候 SELECT 語句的查詢結(jié)果集就會送到游標工作區(qū)。
第三步,從游標中取得數(shù)據(jù)。
FETCH cursor_name INTO var_name ...
這句的作用是使用 cursor_name 這個游標來讀取當前行,并且將數(shù)據(jù)保存到 var_name 這個變量中,游標指針指到下一行。如果游標讀取的數(shù)據(jù)行有多個列名,則在 INTO 關(guān)鍵字后面賦值給多個變量名即可。
第四步,關(guān)閉游標。
CLOSE cursor_name
有 OPEN 就會有 CLOSE,也就是打開和關(guān)閉游標。當我們使用完游標后需要關(guān)閉掉該游標。關(guān)閉游標之后,我們就不能再檢索查詢結(jié)果中的數(shù)據(jù)行,如果需要檢索只能再次打開游標。
最后一步,釋放游標。
DEALLOCATE PREPARE
有 DECLARE 就需要有 DEALLOCATE,DEALLOCATE 的作用是釋放游標。我們一定要養(yǎng)成釋放游標的習慣,否則游標會一直存在于內(nèi)存中,直到進程結(jié)束后才會自動釋放。當你不需要使用游標的時候,釋放游標可以減少資源浪費。
我先創(chuàng)建一個存儲過程 calc_hp_max,然后在存儲過程中定義游標 cur_hero,使用 FETCH 獲取每一行的具體數(shù)值,然后賦值給變量 hp,再用變量 hp_sum 做累加求和,最后再輸出 hp_sum,代碼如下:
CREATE PROCEDURE `calc_hp_max`()
BEGIN
-- 創(chuàng)建接收游標的變量
DECLARE hp INT;
-- 創(chuàng)建總數(shù)變量
DECLARE hp_sum INT DEFAULT 0;
-- 創(chuàng)建結(jié)束標志變量
DECLARE done INT DEFAULT false;
-- 定義游標
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
OPEN cur_hero;
read_loop:LOOP
FETCH cur_hero INTO hp;
SET hp_sum = hp_sum + hp;
END LOOP;
CLOSE cur_hero;
SELECT hp_sum;
DEALLOCATE PREPARE cur_hero;
END
你會發(fā)現(xiàn)執(zhí)行call calc_hp_max()這一句的時候系統(tǒng)會提示 1329 錯誤,也就是在 LOOP 中當游標沒有取到數(shù)據(jù)時會報的錯誤。

當游標溢出時(也就是當游標指向到最后一行數(shù)據(jù)后繼續(xù)執(zhí)行會報的錯誤),我們可以定義一個 continue 的事件,指定這個事件發(fā)生時修改變量 done 的值,以此來判斷游標是否已經(jīng)溢出,即:
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
同時在循環(huán)中我們需要加上對 done 的判斷,如果游標的循環(huán)已經(jīng)結(jié)束,就需要跳出 read_loop 循環(huán),完善的代碼如下:
CREATE PROCEDURE `calc_hp_max`()
BEGIN
-- 創(chuàng)建接收游標的變量
DECLARE hp INT;
-- 創(chuàng)建總數(shù)變量
DECLARE hp_sum INT DEFAULT 0;
-- 創(chuàng)建結(jié)束標志變量
DECLARE done INT DEFAULT false;
-- 定義游標
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
-- 指定游標循環(huán)結(jié)束時的返回值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
OPEN cur_hero;
read_loop:LOOP
FETCH cur_hero INTO hp;
-- 判斷游標的循環(huán)是否結(jié)束
IF done THEN
LEAVE read_loop;
END IF;
SET hp_sum = hp_sum + hp;
END LOOP;
CLOSE cur_hero;
SELECT hp_sum;
DEALLOCATE PREPARE cur_hero;
END

在游標中的循環(huán)中,除了使用 LOOP 循環(huán)以外,你還可以使用 REPEAT… UNTIL…以及 WHILE 循環(huán)。它們同樣需要設(shè)置 CONTINUE 事件來處理游標溢出的情況。
所以你能看出,使用游標可以讓我們對 SELECT 結(jié)果集中的每一行數(shù)據(jù)進行相同或者不同的操作,從而很精細化地管理結(jié)果集中的每一條數(shù)據(jù)。
使用游標來解決一些常見的問題
我剛才講了一個簡單的使用案例,實際上如果想要統(tǒng)計 hp_sum,完全可以通過 SQL 語句來完成,比如:
SELECT SUM(hp_max) FROM heros;

那么游標都有什么用呢?
當你需要處理一些復雜的數(shù)據(jù)行計算的時候,游標就會起到作用了。我舉個例子,還是針對 heros 數(shù)據(jù)表,假設(shè)我們想要對英雄的物攻成長(對應(yīng) attack_growth)進行升級,在新版本中大范圍提升英雄的物攻成長數(shù)值,但是針對不同的英雄情況,提升的幅度也不同,具體提升的方式如下。
如果這個英雄原有的物攻成長小于 5,那么將在原有基礎(chǔ)上提升 7%-10%。如果物攻成長的提升空間(即最高物攻 attack_max- 初始物攻 attack_start)大于 200,那么在原有的基礎(chǔ)上提升 10%;如果物攻成長的提升空間在 150 到 200 之間,則提升 8%;如果物攻成長的提升空間不足 150,則提升 7%。
如果原有英雄的物攻成長在 5—10 之間,那么將在原有基礎(chǔ)上提升 5%。
如果原有英雄的物攻成長大于 10,則保持不變。
以上所有的更新后的物攻成長數(shù)值,都需要保留小數(shù)點后 3 位。
你能看到上面這個計算的情況相對復雜,實際工作中你可能會遇到比這個更加復雜的情況,這時你可以采用面向過程的思考方式來完成這種任務(wù),也就是說先取出每行的數(shù)值,然后針對數(shù)值的不同情況采取不同的計算方式。
針對上面這個情況,你自己可以用游標來完成轉(zhuǎn)換,具體的代碼如下:
CREATE DEFINER=`root`@`localhost` PROCEDURE `alter_attack_growth`()
BEGIN
-- 創(chuàng)建接收游標的變量
DECLARE temp_id INT;
DECLARE temp_growth, temp_max, temp_start, temp_diff FLOAT;
-- 創(chuàng)建結(jié)束標志變量
DECLARE done INT DEFAULT false;
-- 定義游標
DECLARE cur_hero CURSOR FOR SELECT id, attack_growth, attack_max, attack_start FROM heros;
-- 指定游標循環(huán)結(jié)束時的返回值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
OPEN cur_hero;
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
REPEAT
IF NOT done THEN
SET temp_diff = temp_max - temp_start;
IF temp_growth < 5 THEN
IF temp_diff > 200 THEN
SET temp_growth = temp_growth * 1.1;
ELSEIF temp_diff >= 150 AND temp_diff <=200 THEN
SET temp_growth = temp_growth * 1.08;
ELSEIF temp_diff < 150 THEN
SET temp_growth = temp_growth * 1.07;
END IF;
ELSEIF temp_growth >=5 AND temp_growth <=10 THEN
SET temp_growth = temp_growth * 1.05;
END IF;
UPDATE heros SET attack_growth = ROUND(temp_growth,3) WHERE id = temp_id;
END IF;
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
UNTIL done = true END REPEAT;
CLOSE cur_hero;
-- DEALLOCATE PREPARE cur_hero;
END
這里我創(chuàng)建了 alter_attack_growth 這個存儲過程,使用了 REPEAT…UNTIL…的循環(huán)方式,針對不同的情況計算了新的物攻成長 temp_growth,然后對原有的 attack_growth 進行了更新,最后調(diào)用 call alter_attack_growth(); 執(zhí)行存儲過程。
有一點需要注意的是,我們在對數(shù)據(jù)表進行更新前,需要備份之前的表,我們可以將備份后的表命名為 heros_copy1。更新完 heros 數(shù)據(jù)表之后,你可以看下兩張表在 attack_growth 字段上的對比,我們使用 SQL 進行查詢:
SELECT heros.id, heros.attack_growth, heros_copy1.attack_growth FROM heros JOIN heros_copy1 WHERE heros.id = heros_copy1.id;

通過前后兩張表的 attack_growth 對比你也能看出來,存儲過程通過游標對不同的數(shù)據(jù)行進行了更新。
需要說明的是,以上代碼適用于 MySQL,如果在 SQL Server 或 Oracle 中,使用方式會有些差別。