游标是「一次处理一行」的机制:声明、打开、取一行、处理、关掉。 它是 SQL 里少见的过程式工具,也是被滥用最多的一件。 Demo 用 Python 的 cursor 逐行处理十一条明细: 数量不小于 10 的三项打了 95 折、折扣合计 59.24; 同样两个数一条 SQL 就出来了——这就是「能用集合操作就别用游标」的实证。
上一章的存储过程给了「写流程」的能力,这一章给「逐行」的能力。 游标和集合操作是两条路:一条一行一行走,一条一批算完。 本课前面所有章节走的都是后者,这一章讲前者为什么是最后手段。
一、游标:一次处理一行
SQL 天生是「集合」的语言:一句 UPDATE 影响一批行, 你不需要、也没法指定「先处理第三行再处理第四行」。 但有些事必须逐行做:调外部接口、写文件、或者后一行的计算要用前一行的结果。 游标就是为这些情况准备的。 它的代价是慢:一万行就要来回一万次, 而集合操作是一批下去一次算完。 所以教材的立场很清楚:游标是最后手段,不是首选工具。
二、五步:声明、打开、取、循环、关闭
DECLARE 游标名 CURSOR FOR SELECT ...:声明,此时并不执行那条 SELECT。
OPEN 游标名:真正执行查询,把结果集准备好。
FETCH 游标名 INTO 变量...:取一行,塞进变量。
循环靠「取不到就退出」:MySQL 用 CONTINUE HANDLER FOR NOT FOUND 设一个标志,
每轮检查那个标志。 这一步是最容易写错的地方。
CLOSE 游标名:关掉,释放资源。不关会一直占着,游标数量还有上限。
这五步必须在存储过程里——MySQL 不允许在普通 SQL 里直接声明游标。
所以第 23 章是这一章的前置条件。
三、本沙箱怎么跑:Python 的 cursor
c.execute(...) 返回的对象本身就是游标,fetchone() 取一行、fetchall() 取全部、
直接 for 循环也行。 这和 DECLARE/OPEN/FETCH/CLOSE 是一一对应的。
Demo 逐行走了十一条明细:数量不小于 10 的打 95 折,折扣逐笔累加,合计 59.24。
最后一定要验算:逐行数出来的行数必须等于 COUNT(*),
不然就是循环写漏了——这是游标代码最常见的 bug。
四、先问一句:能不能用一条 SQL
Demo 第四节用一条 SQL 算出同样的 3 项、59.24。 这就是判断标准:能一句写完就别用游标。 本课这套数据只有 11 行,快慢看不出差别—— 但行数一上去,逐行和集合就是几十倍的差距。 这一点必须说清楚:小数据上「跑得挺快」不能证明游标没问题。 游标真正该用的两个场景:每行要做的事 SQL 表达不了; 行与行之间有依赖。 除了这两条,都优先考虑集合操作。 还有一个折中办法:先用集合操作把「能算的」算完,只把真正需要逐行的那部分交给游标, 别整表都走游标。
五、小结
教材小结: 游标用于逐行处理,五步是声明、打开、取、循环、关闭,必须在存储过程里,且应作为最后手段。 本课补三条:一是能用一条 SQL 就别用游标;二是小数据上看不出慢,别被骗; 三是游标代码必须用 COUNT(*) 验算行数。
六、动手 Demo:逐行走十一行,再一条 SQL 走完
Demo 五段:先看清要处理的数据,再给 MySQL 的五步原码,然后用 Python 的 cursor 真跑一遍, 接着用一条 SQL 做同样的事,最后讲该用在哪。 重点看两处:折扣合计 59.24,以及逐行数出的 11 行与 COUNT(*) 对得上。
import sqlite3
con = sqlite3.connect(":memory:")
# 游标 = 一次处理一行;c.execute(...) 返回的就是游标对象
cur = con.execute("SELECT order_num, prod_id, quantity, item_price "
"FROM orderitems ORDER BY order_num")
rows = cur.fetchall() # 对应 FETCH ALL
n = 0
disc = 0.0
for onum, pid, qty, price in rows: # 对应 FETCH 一行一行取
n += 1
if qty >= 10:
disc += qty * price * 0.05
print("行数", n, "折扣合计", round(disc, 2))
# 同样两个数,一条 SQL 就够——能用集合操作就别用游标
con.execute("SELECT COUNT(*) AS n, ROUND(SUM(quantity * item_price * 0.05), 2) "
"AS disc FROM orderitems WHERE quantity >= 10")
# 核对:逐行数的行数必须等于 COUNT(*)
con.execute("SELECT COUNT(*) AS rows_total, SUM(quantity) AS qty_total "
"FROM orderitems")
实跑输出:
--------------------------------------------------------------------
第二十四课:使用游标
--------------------------------------------------------------------
【一】游标解决什么问题:逐行处理
SQL> SELECT order_num, prod_id, quantity, item_price FROM orderitems ORDER BY order_num
order_num prod_id quantity item_price
--------------------------------------------------
20005 ANV01 10 5.99
20005 ANV02 3 9.99
20005 TNT2 5 10.00
20005 FB 1 10.00
20006 JP2000 1 55.00
20007 TNT2 100 10.00
20008 FC 50 2.50
20009 FB 1 10.00
20009 OL1T 1 8.99
20009 SLING 1 4.49
20009 TNT1 1 2.50
(11 行)
**十一条明细。** 现在要求:**数量不小于 10 的那些项,单价打 95 折,并把折扣额累加。**
**先说清楚:这个例子第四节会用一句 SQL 算出来,比游标干净得多。**
这里只是借它演示「一次处理一行」的动作——
**真正的业务里往往是「每行要做的事 SQL 表达不了」,那才是游标的用武之地。**
【二】MySQL 的五步(教材原码,不执行)
`DECLARE 游标名 CURSOR FOR SELECT ...;` **声明**
`OPEN 游标名;` **打开**
`FETCH 游标名 INTO 变量...;` **取一行**
循环取到没有为止(靠 `CONTINUE HANDLER FOR NOT FOUND` 退出)
`CLOSE 游标名;` **关闭**
**五步一个不能少**:不声明没得用,不打开取不到,不关闭会占着资源。
**这几句必须在存储过程里,MySQL 不允许在普通 SQL 里直接声明游标。**
【三】本沙箱怎么真跑:用 Python 的 cursor 迭代
**`c.execute(...)` 返回的就是一个游标**,`fetchone()` 取一行、`fetchall()` 取全部。
**和 DECLARE/OPEN/FETCH/CLOSE 是一一对应的。**
第 1 行:单号 20005 产品 ANV01 数量 10,单价 5.99,折扣 3.00
第 2 行:单号 20005 产品 ANV02 数量 3,单价 9.99,不打折
第 3 行:单号 20005 产品 TNT2 数量 5,单价 10.00,不打折
第 4 行:单号 20005 产品 FB 数量 1,单价 10.00,不打折
第 5 行:单号 20006 产品 JP2000 数量 1,单价 55.00,不打折
第 6 行:单号 20007 产品 TNT2 数量 100,单价 10.00,折扣 50.00
第 7 行:单号 20008 产品 FC 数量 50,单价 2.50,折扣 6.25
第 8 行:单号 20009 产品 FB 数量 1,单价 10.00,不打折
第 9 行:单号 20009 产品 OL1T 数量 1,单价 8.99,不打折
第 10 行:单号 20009 产品 SLING 数量 1,单价 4.49,不打折
第 11 行:单号 20009 产品 TNT1 数量 1,单价 2.50,不打折
**一共 11 行,折扣合计 59.24。** 逐行取、逐行判、逐行累加,**这就是游标的典型用法。**
**再看总数对不对:**
SQL> SELECT COUNT(*) AS rows_total, SUM(quantity) AS qty_total FROM orderitems
rows_total qty_total
--------------------------
11 174
(1 行)
**11 行、174 件,和逐行数出来的一致**,说明游标没漏行。
【四】能用一条 SQL 就别用游标
SQL> SELECT COUNT(*) AS n, ROUND(SUM(quantity * item_price * 0.05),
2) AS disc FROM orderitems WHERE quantity >= 10
n disc
------------
3 59.24
(1 行)
**同样两个数,一条 SQL 就出来了**,而且引擎可以并行、可以走索引。
**游标是「逐行」的,天生慢**:一万行就要来回一万次,**而集合操作是一批下去一次算完。**
**这条原则要记牢:能用集合操作解决的,不要用游标。**
**本课这套数据只有 11 行,快慢看不出差别;行数一上去就是几十倍的差距。**
【五】游标真正该用的地方
**一是「每行要做的事没法用 SQL 表达」**:调外部接口、写文件、按复杂规则改状态。
**二是「行与行之间有依赖」**:后一行的计算要用前一行的结果。
**除此之外都优先考虑集合操作**——这也是教材反复强调「游标是最后手段」的原因。
第一段摆数据。 十一条明细,quantity 从 1 到 100 都有。
第二段五步原码。 声明、打开、取、循环、关闭,一句都不能省。
第三段逐行跑。 前三项数量不小于 10:10 件、100 件、50 件,
折扣 3.00、50.00、6.25,合计 59.24;
最后验算 11 行、174 件,和 COUNT/SUM 一致,说明循环没漏行。
第四段一条 SQL。 同样给 3 与 59.24,而且引擎能走索引。
第五段不给输出,只给判断标准:两条之外都优先用集合操作。
30 秒实验
- 把门槛从
quantity >= 10改成>= 5,折扣合计变成多少? - 用一条 SQL 算「每个订单的折扣额」,和逐行版对得上吗?
- (先想再跑)游标只遍历前五行就 break,验算那步会对不上吗?
常见误区
- 拿游标处理整表。能用一条 SQL 解决的事,逐行做会慢几十倍。
- 忘了 CLOSE。游标不关会一直占资源,连接池很快就耗尽。
- 不验算行数。循环写漏一行不报错,只是结果少了一点,很难发现。
自测三问(点开对答案)
1. 游标的五个步骤是什么? —— DECLARE 声明、OPEN 打开、FETCH 取一行、循环到取不到为止、CLOSE 关闭。 2. 为什么说游标是「最后手段」? —— 它逐行处理,一万行就要来回一万次;集合操作一批算完,快得多,也更能利用索引。 3. 什么情况下确实该用游标? —— 每行要做的事 SQL 表达不了(调接口、写文件),或者行与行之间有依赖。视图、存储过程、游标,三件「服务端」工具齐了。 还剩一件:让数据库在数据变化的那一刻自动做事——触发器。
- ☐ (本章完,下一章 第25章 使用触发器)