阅读整理自《MySQL 必知必会》- 朱晓峰,详细内容请登录 极客时间 官网购买专栏。
游标,也就是能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。
游标的使用步骤
游标只能在存储程序内使用,存储程序包括存储过程和存储函数。
create function 函数名称(参数) return 数据类型 程序体
存储函数与存储过程很像,但有几个不同点:
- 存储函数必须返回一个值或者数据表,存储过程可以不返回
- 存储过程可以通过 CALL 语句调用,存储函数不可以
- 存储函数可以放在查询语句中使用,存储过程不行
- 存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的
在使用游标的时候,主要有 4 个步骤
-
定义游标
declare 游标名 cursor for 查询语句;
声明一个游标,它可以操作的数据集是“查询语句”返回的结果集。
-
打开游标
open 游标名称;
打开游标之后,系统会为游标准备好查询的结果集,为后面游标的逐条读取结果集中的记录做准备。
扫描二维码关注公众号,回复: 16049128 查看本文章 -
从游标的数据结果集中读取数据
fetch 游标名 into 变量列表;
通过游标,把当前游标指向的结果集中那一条记录的数据,赋值给列表中的变量。
游标的查询结果集中的字段数,必须跟 into 后面的变量数一致,否则,在存储过程执行的时候,MySQL 会提示错误。
-
关闭游标
close 游标名;
用完游标之后,一定要记住及时关闭游标。因为游标会占用系统资源,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放游标占用的系统资源。
案例
在超市项目的进货模块中,有一项功能是对进货单数据进行验收。其实就是在对进货单的数据确认无误后,对进货单的数据进行处理,包括增加进货商品的库存,并修改商品的平均进价。
进货单头表(demo.importhead)
Listnumber | Supplierid | Stockid | Operatorid | Totalquantity | Totalvalue | Recordingdate |
---|---|---|---|---|---|---|
1234 | 1 | 1 | 1 | 8 | 171 | 2020-12-12 |
进货单明细表(demo.importdetails)
Listnumber | Itemnumber | Quantity | Importprice | Importvalue |
---|---|---|---|---|
1234 | 1 | 5 | 33 | 165 |
1234 | 2 | 3 | 2 | 6 |
库存表(demo.inventory)
Stocknumber | Itemnumber | Invquantity |
---|---|---|
1 | 1 | 10 |
1 | 2 | 20 |
商品信息表(demo.goodsmaster)
Itemnuber | Barcode | Goodsname | Specification | Unit | Salesprice | Avgimportprice |
---|---|---|---|---|---|---|
1 | 0001 | 书 | 16开 | 本 | 89 | 30 |
2 | 0002 | 笔 | NULL | 支 | 5 | 3 |
要验收进货单,就需要对每一个进货商品进行两个操作:
- 在现有库存数量的基础上,加上本次进货的数量;
- 根据本次进货的价格、数量,现有商品的平均进价和库存,计算新的平均进价:(本次进货价格 * 本次进货数量 + 现有商品平均进价 * 现有商品库存)/(本次进货数量 + 现有库存数量)。
因为需要通过应用程序来控制操作流程,做成一个循环操作,每次只查询一种商品的数据记录并进行处理,一直到把进货单中的数据全部处理完。这样一来,应用必须发送很多的 SQL 指令到服务器,跟服务器的交互多,不仅代码复杂,而且也不够安全。如果使用游标,就很容易了。因为所有的操作都可以在服务器端完成,应用程序只需要发送一个命令调用存储过程就可以了。
创建了一个存储过程 demo.mytest()
mysql> delimiter //
mysql> create procedure demo.mytest(mylistnumber int)
-> begin
-> declare mystockid int;
-> declare myitemnumber int;
-> declare myquantity decimal(10,3);
-> declare myprice decimal(10,2);
-> declare done int default FALSE; -- 用来控制循环结束
-> declare cursor_importdata cursor for -- 定义游标
-> select b.stockid, a.itemnumber, a.quantity, a.importprice
-> from demo.importdetails as a
-> join demo.importhead as b
-> on (a.listnumber=b.listnumber)
-> where a.listnumber = mylistnumber;
-> declare continue handler for not found set done = true; -- 条件处理语句
->
-> open cursor_importdata; -- 打开游标
-> fetch cursor_importdata into mystockid, myitemnumber, myquantity, myprice; -- 读入第一条记录
-> repeat
-> -- 更新商品进价
-> update demo.goodsmaster as a, demo.inventory as b
-> set a.avgimportprice = (a.avgimportprice*b.invquantity+myprice*myquantity)/(b.invquantity+myquantity)
-> where a.itemnumber=b.itemnumber and b.stockid=mystockid and a.itemnumber=myitemnumber;
-> -- 更新商品库存
-> udpate demo.inventory
-> set invquantity = invquantity + myquantity
-> where stockid = mystockid and itemnumber=myitemnumber;
-> -- 获取下一条记录
-> fetch cursor_importdata into mystockid, myitemnumber, myquantity, myprice;
-> until done end repeat;
-> close cursor_importdata;
-> end
-> //
Query OK, 0 rows affected (0.02 sec)
-> delimiter ;
这段代码核心操作有 6 步:
- 把 MySQL 的分隔符改成“//”
- 开始程序体之后,定义了 4 个变量,分别是 mystockid、myitemnumber、myquantity 和 myprice,这几个变量的作用是,存储游标中读取的仓库编号、商品编号、进货数量和进货价格数据。
- 定义游标。这里指定了游标的名称,以及游标可以处理的数据集(mylistnumber 指定的进货单的全部进货商品明细数据)。
- 定义条件处理语句 declare continue handler for not found set done = true; 。
- 打开游标,读入第一条记录,然后开始执行数据操作。
- 关闭游标,结束程序。
处理进货单验收的存储过程就创建好了。运行一下这个存储过程,看看能不能得到想要的结果:
mysql> CALL demo.mytest(1234); -- 调用存储过程,验收单号是1234的进货单
Query OK, 0 rows affected (11.68 sec) -- 执行成功了
mysql> select * from demo.inventory; -- 查看库存,已经改过来了
+---------+------------+-------------+
| stockid | itemnumber | invquantity |
+---------+------------+-------------+
| 1 | 1 | 15.000 |
| 1 | 2 | 23.000 |
+---------+------------+-------------+
2 rows in set (0.00 sec)
mysql> select * from demo.goodsmaster; -- 查看商品信息表,平均进价也改过来了
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.00 sec)
语句
条件处理语句
declare 处理方式 handler for 问题 操作;
- 语法结构中的“问题”是指 SQL 操作中遇到了什么问题。比如这里的问题是“NOT FOUND”,意思就是游标走到结果集的最后,没有记录了。也就是说,数据集中的所有记录都已经处理完了。
- 执行的操作是“SET done=TRUE”,done 是定义的用来标识数据集中的数据是否已经处理完成的一个标记。done=TRUE,意思是数据处理完成了。
- 处理方式有 2 种选择,分别是“CONTINUE”和“EXIT”,表示遇到问题,执行了语法结构中的“操作”之后,是选择继续运行程序,还是选择退出,结束程序。
流程控制语句
MySQL 的流程控制语句也只能用于存储程序,主要有 3 类。
-
跳转语句:iterate 和 leave 语句
- iterate 只能用在循环语句内,表示重新开始循环
- leave 可以用在循环语句内,或者以 begin 和 end 包裹起来的程序体内,表示跳出循环或者跳出程序体的操作
-
循环语句:loop、while 和 repeat 语句
-
loop
标签:LOOP 操作 END LOOP 标签;
-
while
WHILE 条件 DO 操作 END WHILE;
WHILE 循环通过判断条件是否为真来决定是否继续执行循环中的操作,要注意一点,WHILE 循环是先判断条件,再执行循环体中的操作。
-
repeat
REPEAT 操作 UNTIL 条件 END REPEAT;
REPEAT 循环也是通过判断条件是否为真来决定是否继续执行循环内的操作的,与 WHILE 不同的是,REPEAT 循环是先执行操作,后判断条件。
-
-
条件判断语句:if 语句和 case 语句
-
if
IF 表达式1 THEN 操作1 [ELSEIF 表达式2 THEN 操作2]…… [ELSE 操作N] END IF
IF 语句的特点是,不同的表达式对应不同的操作
-
case
CASE 表达式 WHEN 值1 THEN 操作1 [WHEN 值2 THEN 操作2]…… [ELSE 操作N] END CASE;
CASE 语句的特点是,表达式不同的值对应不同的操作。
-
有个小问题要注意:如果一个操作要用到另外一个操作的结果,那一定不能搞错操作的顺序。
测试题
假设有一个数据表 test.cursor,具体信息如下所示:
mysql> select * from test.table_cursor;
+----+----------+
| id | quantity |
+----+----------+
| 1 | 100 |
| 2 | 101 |
| 3 | 102 |
| 4 | 103 |
+----+----------+
写一个简单的存储过程,用游标来逐一处理一个数据表中的数据,要求:编号为偶数的记录,quanit=quanit+1;编号是奇数的记录,quanit=quanit+2。
delimiter //
create procedure test.procedure_cursor()
begin
declare t_id int;
declare t_quantity int;
declare done int default false;
declare t_cursor cursor for select * from test.table_cursor;
declare continue handler for not found set done = true;
open t_cursor;
fetch t_cursor into t_id, t_quantity;
repeat
if (t_id mod 2 = 0) then
update test.table_cursor set quantity = quantity + 1 where id = t_id;
else
update test.table_cursor set quantity = quantity + 2 where id = t_id;
end if;
fetch t_cursor into t_id, t_quantity;
until done end repeat;
close t_cursor;
end
//
delimiter ;
调用:
mysql> call test.procedure_cursor();
Query OK, 0 rows affected (0.03 sec)
mysql> select * from test.table_cursor;
+----+----------+
| id | quantity |
+----+----------+
| 1 | 102 |
| 2 | 102 |
| 3 | 104 |
| 4 | 104 |
+----+----------+
4 rows in set (0.00 sec)