1.游標說明
游標,有些地方也稱為光標。它的作用是在一個結果集中逐條逐條地獲取記錄行並操作它們。
例如:
其中select是游標所操作的結果集,游標每次fetch一行中的name和age字段,並將每一行的這兩個字段賦值給變量var1和var2。
有很多、很多、很多人,很多、很多、很多書都強烈建議:能不用游標盡量不要用游標。因為它違背了集合的理論,集合取數據是一把一把抓,游標取數據的時候一行一行取,每取一行操作一行,而且在每一行上都有額外的資源消耗。總之,游標效率低、資源消耗高。
其實很多領域都有這樣的優化:把數據先集中起來,集中到了一定量再一次性處理,這樣的處理方式效率要高得多。比如寫日志到磁盤上,可以每產生一條日志就刷入磁盤,也可以先產生一堆日志緩存起來,之后一次性刷如磁盤。后者效率要高得多。
集合取數據的時候關注點在於想要什么數據,而不關注怎么去獲取數據,游標的關注點則在於怎么獲取這些數據:將游標指針作為遍歷依據,遍歷到哪行數據就返回這行數據然后停下來處理數據,再繼續遍歷數據。習慣於迭代的人比較喜歡游標,特別是習慣C語言的人,因為游標就是遍歷數據行的行為。
在MySQL、MariaDB中實現的游標比較簡單,它只有一種遍歷方式:逐行向前遍歷。MariaDB 10.3后,游標方面支持的更完整一點:支持游標參數。
光標的使用包括聲明光標、打開光標、使用光標和關閉光標(MySQL/MariaDB中的游標無需釋放)。光標必須聲明在處理程序之前,並且在聲明保存結果集的變量之后。另外,游標是一種復合語句結構(就像begin...end),只能用於stored procedure或stored function中。
2.使用游標
1.聲明游標
DECLARE cursor_name CURSOR FOR select_statement;
其中select_statement是游標需要從中獲取的結果集。
例如:
declare cur_city cursor for select id,'name',population from world,city;
在MariaDB 10.3中,支持游標參數,該參數可以傳遞到select_statement中:
DECLARE cursor_name CURSOR(param1 data_type,param2 data_type2...) FOR select_statement;
例如:
declare cur_stu cursor(min int,max int) for select id,name from Student where id between min and max;
注意,mariaDB 10.3之前的語法也能在10.3版本上執行,因為之前的語法是10.3版本中不帶參數的特殊情況。
2.聲明處理程序
一般來說,光標是用在逐條取結果集的情況下,所以在使用光標的時候基本都會放在循環結構中循環獲取數據存儲到變量中。但如何在取完數據后退出循環?
在游標無法獲取到下一行數據的時候,將會返回一個1329錯誤碼,這個錯誤碼對應的SQL狀態碼為"02000",它們等價於NOT FOUND(這幾個是等價的,只是MariaDB中分了3類描述問題的代碼而已)。這時可以在聲明游標后定義一個handler,用於處理NOT FOUND。
例如下面是適合游標NOT FOUND時的CONTINUE處理器,表示當找不到下一行數據時繼續執行后面的程序:
DECLARE CONTINUE HANDLER FOR NOT FOUND statement;
對於處理游標的HANDLER,通常statement部分是SET語句,用於設置一些變量。例如:
declare continue handler for not found set var_name=value;
這時,當取不到下一條記錄時即已經取完記錄時,就設置變量var_name=value。之后就可以通過該變量的值作為退出循環的條件。
關於handler詳細內容,見我翻譯的MariaDB手冊:https://mariadb.com/kb/zh-cn/declare-handler/
3.打開游標
當聲明了一個游標后,必須要打開游標才能使用游標。
open cursor_name;
例如:
open cur_city;
對於mariadb 10.3,由於支持游標參數,因此語法為:
open cursor_name(value1,value2);
例如:
open cur_stu(4,10);
4.使用游標(fetch into)
通過fetch into命令將每次fetch到的結果存儲到預先定義好的變量中。注意,這個變量必須是本地變量(局部變量),不能是用戶自定義變量,且這個變量必須定義在游標聲明語句之前。
fetch cursor_name into var_name;
例如:
fetch cur_city into city_id,city_name,city_popcnt;
在上面已經說過了,一般游標都會在循環結構中使用。以下是在repeat結構中使用游標;
repeat
fetch ... into ...
until var_name=value
end repeat;
5.關閉游標
close cursor_name;
例如:
close cur_city;
3.游標使用示例
以下是MariaDB 10.3版本之前(也適用於10.3)的游標使用示例:將表t1和表t2中每行中的某一列作比較,將較大值插入到表t3中。
create or replace table t1(i int);
create or replace table t2(i int);
create or replace table t3(i int);
insert into t1 values(5),(10),(20);
insert into t2 values(15),(30),(10);
delimiter $$
create or replace procedure proc1() begin declare done int default false; /* 用於判斷退出循環 */
declare x,y int; /* 用於保存fetch結果 */
declare cur1 cursor for select i from t1; /* fetch t1的游標 */
declare cur2 cursor for select i from t2; /* fetch t2的游標 */
declare continue handler for not found set done=true; /* not found時,退出循環 */
open cur1;
open cur2;
my_loop: LOOP
fetch cur1 into x;
fetch cur2 into y;
if done then
leave my_loop;
end if;
if x <= y then insert into t3 values(y);
else insert into t3 values(x);
end if;
end loop;
close cur1;
close cur2;
end$$
delimiter ;
call proc1;
查看表t3:
select * from t3;
+------+
| i |
+------+
| 15 |
| 30 |
| 20 |
+------+
下面是MariaDB 10.3上使用游標的一個示例:將表t1中i字段某一段數據插入到表t2中。
create or replace table t1(i int);
create or replace table t2(i int);
insert into t1 values(5),(10),(20),(30),(40);
delimiter $$
create or replace procedure proc1(min int,max int) begin declare done int default false;
declare x int;
declare cur1 cursor(cmin int,cmax int) for select i from t1 where t1.i between cmin and cmax;
declare continue handler for not found set done=true;
open cur1(min,max);
my_loop: LOOP
fetch cur1 into x;
if done then
leave my_loop;
end if;
insert into t2 values(x);
end loop;
close cur1;
end$$ delimiter ;
call proc1(10,40);
查看t2結果:
MariaDB [test]> select * from t2;
+------+
| i |
+------+
| 10 |
| 20 |
| 30 |
| 40 |
+------+