🐬 第24章 使用游标
📄 一句话概括

游标是「一次处理一行」的机制:声明、打开、取一行、处理、关掉。 它是 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 秒实验

  1. 把门槛从 quantity >= 10 改成 >= 5,折扣合计变成多少?
  2. 用一条 SQL 算「每个订单的折扣额」,和逐行版对得上吗?
  3. (先想再跑)游标只遍历前五行就 break,验算那步会对不上吗?

常见误区

  1. 拿游标处理整表。能用一条 SQL 解决的事,逐行做会慢几十倍。
  2. 忘了 CLOSE。游标不关会一直占资源,连接池很快就耗尽。
  3. 不验算行数。循环写漏一行不报错,只是结果少了一点,很难发现。
自测三问(点开对答案) 1. 游标的五个步骤是什么? —— DECLARE 声明、OPEN 打开、FETCH 取一行、循环到取不到为止、CLOSE 关闭。 2. 为什么说游标是「最后手段」? —— 它逐行处理,一万行就要来回一万次;集合操作一批算完,快得多,也更能利用索引。 3. 什么情况下确实该用游标? —— 每行要做的事 SQL 表达不了(调接口、写文件),或者行与行之间有依赖。

视图、存储过程、游标,三件「服务端」工具齐了。 还剩一件:让数据库在数据变化的那一刻自动做事——触发器。

相关笔记