五步:DECLARE 定义、OPEN 打开、FETCH 取、CLOSE 关闭、DEALLOCATE 释放。
FETCH 要写两次:循环前一次、循环末尾一次,靠 @@FETCH_STATUS = 0 判断还能不能取。
最容易漏的是 CLOSE 和 DEALLOCATE——当下不报错,锁和内存一直挂着,问题在后面才爆发。
13.1 讲「游标是什么、为什么要少用」,这一节讲「怎么写」——五个动作一个不能少。
教材 13.2 分五小节,正好五步一一对应。「小结」和「实践与练习」按惯例并进本篇末尾。
SQLite 没有这套语法,演示里用 Python 的 cursor 一一对照着走:
execute() 相当于 DECLARE + OPEN,fetchone() 相当于 FETCH,
close() 相当于 CLOSE 加 DEALLOCATE。其中那个「逐行打折」的表是真跑出来的。
一、五步原码
DECLARE cur_goods CURSOR FOR
SELECT id, gname, price FROM tb_goods WHERE is_on = 1 ORDER BY id;
OPEN cur_goods;
DECLARE @id INT, @name NVARCHAR(50), @price MONEY;
FETCH NEXT FROM cur_goods INTO @id, @name, @price;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT CAST(@id AS VARCHAR(10)) + ' ' + @name;
FETCH NEXT FROM cur_goods INTO @id, @name, @price;
END;
CLOSE cur_goods;
DEALLOCATE cur_goods;
GO
| 步骤 | 干什么 | 漏了会怎样 |
|---|---|---|
DECLARE |
定义游标和它的查询 | 后面全用不了 |
OPEN |
执行查询、把结果集装进游标 | FETCH 报「游标未打开」 |
FETCH |
取下一行,塞进变量 | 取不到数据 |
CLOSE |
关掉游标,释放锁 | 锁一直占着,别人改不了表 |
DEALLOCATE |
彻底释放游标占的内存 | 内存慢慢被吃掉 |
注意 FETCH 出现了两次。 循环前那次是「先把第一行取出来」,
循环末尾那次是「处理完再取下一条」——漏了循环前那次,第一行会被漏掉。
二、用 Python 的游标把这五步走一遍
cur = c.cursor() # DECLARE
cur.execute(u"SELECT ... FROM ...") # OPEN
row = cur.fetchone() # FETCH
cur.close() # CLOSE + DEALLOCATE
能一一对上不是巧合。 游标的本质就是「结果集 + 当前位置」,
Python 的 cursor 也是这个东西——所以它的名字就叫 cursor。
演示里取回 5 行,跟 SQL 直接查出来的完全一样。 游标不改变结果,只改变「怎么拿」。
区别在于「停在中间」。 直接查是一次性把结果搬回来;
游标可以取一行、处理一下、再取一行,中间这个过程是你的代码说了算。
三、@@FETCH_STATUS:循环靠它停
| 返回值 | 含义 | 该怎么办 |
|---|---|---|
| 0 | 取到行了 | 处理,然后再 FETCH 一次 |
| -1 | 越界了(没有更多行) | 跳出循环 |
| -2 | 这一行已经被删了 | 跳过,继续 FETCH |
标准写法是「先 FETCH 一次,再 WHILE @@FETCH_STATUS = 0,循环里处理完再 FETCH」。
这个结构写成模板最保险——自己每次重编,早晚漏一次。
-2 那个状态只在部分游标类型下出现(比如键集驱动游标,读到一半别人把这行删了)。
只进游标基本碰不到,但代码里判断一下没坏处。
四、真跑一个逐行处理:分类打折
演示里按类别给折扣:1 类 0.95、2 类 0.90、3 类 0.85,其余不打折。
| id | gname | 原价 | 折扣 | 新价 |
|---|---|---|---|---|
| 1001 | 联想笔记本 | 4999.00 | 0.95 | 4749.05 |
| 1003 | 罗技鼠标 | 89.90 | 0.90 | 80.91 |
| 1006 | 打印机 | 899.00 | 0.85 | 764.15 |
每行折扣率不一样,看起来确实像「必须逐行」。 但注意——
这个用一条 CASE 就能算出来(8.3 讲过):
SELECT id, gname, price,
CASE type_id WHEN 1 THEN price * 0.95
WHEN 2 THEN price * 0.90
WHEN 3 THEN price * 0.85
ELSE price END AS 新价
FROM tb_goods WHERE is_on = 1
所以「每行算的东西不一样」不等于「必须用游标」。 只有当规则本身算不出来(要查别的系统、要读文件、要按上一行结果决定),才真需要游标。
五、三个常见坑
| 坑 | 症状 | 怎么避免 |
|---|---|---|
漏了 CLOSE |
表被锁着,别人改不了 | 五步写成模板,配对出现 |
漏了 DEALLOCATE |
内存慢慢涨 | 同上;或者用 TRY ... CATCH 兜住 |
循环里忘了再 FETCH |
死循环 | 套模板,别自己重编 |
第三个坑真会把人熬住:程序不报错、CPU 拉满、数据一行也没改对。
因为它在原地打转,永远处理第一行。
还有一种更隐蔽的:FETCH 写成了 FETCH FIRST。 同样看着在跑,
但每次都取第一行——查日志能看到「处理了 10000 行」,可表里只有一行被改了几万次。
防这个坑的办法只有一个:小数据量先跑一遍,看输出对不对,再上大数据量。
别一上来就跑十万行。
六、动手 Demo:五步走一遍,再看一次真跑的分类打折
这一段贴出五步原码,然后用 Python 的 cursor 一一对照着走一遍,
最后真跑一个「按类别打折」的逐行处理。
注意结尾那张表:每行折扣不同,看起来非游标不可——但一条 CASE 就能替掉它。
import sqlite3
c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据 ...
cur = c.cursor() # DECLARE
cur.execute(u"SELECT id, gname, type_id, price FROM tb_goods "
u"WHERE is_on = 1 ORDER BY id") # OPEN
rules = {1: 0.95, 2: 0.90, 3: 0.85}
rows = []
while True:
row = cur.fetchone() # FETCH
if row is None: # 相当于 @@FETCH_STATUS = -1
break
gid, gname, tid, price = row
rate = rules.get(tid, 1.0)
rows.append((gid, gname, price, rate, round(price * rate, 2)))
cur.close() # CLOSE + DEALLOCATE
print(rows)
实跑输出:
--------------------------------------------------------------------
游标的五个动作:声明、打开、读取、关闭、释放
--------------------------------------------------------------------
① 五步原码
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| DECLARE cur_goods CURSOR FOR
| SELECT id, gname, price FROM tb_goods WHERE is_on = 1 ORDER BY id;
| OPEN cur_goods;
| DECLARE @id INT, @name NVARCHAR(50), @price MONEY;
| FETCH NEXT FROM cur_goods INTO @id, @name, @price;
| WHILE @@FETCH_STATUS = 0
| BEGIN
| PRINT CAST(@id AS VARCHAR(10)) + ' ' + @name;
| FETCH NEXT FROM cur_goods INTO @id, @name, @price;
| END;
| CLOSE cur_goods;
| DEALLOCATE cur_goods;
| GO
| 步骤 | 干什么 | 漏了会怎样 |
|---|---|---|
| DECLARE | 定义游标和它的查询 | 后面全用不了 |
| OPEN | 执行查询、把结果集装进游标 | FETCH 报「游标未打开」 |
| FETCH | 取下一行,塞进变量 | 取不到数据 |
| CLOSE | 关掉游标,释放锁 | 锁一直占着,别人改不了表 |
| DEALLOCATE | 彻底释放游标占的内存 | 内存慢慢被吃掉 |
最容易漏的是后两步。CLOSE 释放锁,DEALLOCATE 释放内存——不写当下不报错,问题在后面。
② 用 Python 的游标把这五步走一遍
--------------------------------------------------------------------
Python 的 cursor 本来就是「结果集 + 当前位置」,所以能一一对上:
DECLARE + OPEN 就是 execute() 那一下——查询跑了,结果集挂在这个 cursor 上。
1> SELECT id, gname, price FROM tb_goods WHERE is_on = 1 ORDER BY id
id gname price
-----------------------
1001 联想笔记本 4999
1002 戴尔显示器 1299
1003 罗技鼠标 89.9
1004 机械键盘 399
1006 打印机 899
(5 行受影响)
取回 5 行,跟 SQL 直接查出来的完全一样——游标不改变结果,只改变「怎么拿」。
③ @@FETCH_STATUS:循环靠它停
--------------------------------------------------------------------
| 返回值 | 含义 | 该怎么办 |
|---|---|---|
| 0 | 取到行了 | 处理,然后再 FETCH 一次 |
| -1 | 越界了(没有更多行) | 跳出循环 |
| -2 | 这一行已经被删了 | 跳过,继续 FETCH |
标准写法是「先 FETCH 一次,再 while @@FETCH_STATUS = 0,循环里处理完再 FETCH」。
注意 FETCH 必须写两次:一次在循环前,一次在循环末尾。漏了循环前那次,会漏掉第一行。
④ 真跑一个「逐行处理」:分类打折
--------------------------------------------------------------------
| id | gname | 原价 | 折扣 | 新价 |
|---:|---|---:|---:|---:|
| 1001 | 联想笔记本 | 4999.00 | 0.95 | 4749.05 |
| 1002 | 戴尔显示器 | 1299.00 | 0.95 | 1234.05 |
| 1003 | 罗技鼠标 | 89.90 | 0.90 | 80.91 |
| 1004 | 机械键盘 | 399.00 | 0.90 | 359.10 |
| 1006 | 打印机 | 899.00 | 0.85 | 764.15 |
每行的折扣率不一样,所以这里确实需要逐行处理——但注意:
也可以用一条 CASE 语句算出来(8.3 讲过)。只有「算不出来」的活,才真的需要游标。
⑤ 三个常见坑
--------------------------------------------------------------------
| 坑 | 症状 | 怎么避免 |
|---|---|---|
| 漏了 CLOSE | 表被锁着,别人改不了 | 用 BEGIN ... END 配对写 |
| 漏了 DEALLOCATE | 内存慢慢涨 | 同上;或者用 TRY ... FINALLY 包住 |
| 循环里忘了再 FETCH | 死循环 | 标准五步写成模板,别自己重编 |
第三行那个死循环真会把人熬住:程序不报错、CPU 拉满、数据一行也没改对。
! 更隐蔽的一种:循环里 FETCH 写成了 FETCH FIRST,每次都取第一行,看着在跑,其实原地打转。
这段代码就是那五步。 cursor() 是 DECLARE,execute() 是 OPEN,fetchone() 是 FETCH,
close() 一下把 CLOSE 和 DEALLOCATE 都干了——
Python 的封装省了后面那步,但概念上要清楚有两个动作。
if row is None 那一行就是 @@FETCH_STATUS = -1。 取不到行了,跳出循环。
T-SQL 里靠状态变量,Python 里靠返回值——判断的都同一件事。
表里那五行,注意最后一列不是简单乘: 1001 是 4749.05、1003 是 80.91。
每行用的折扣率不同,这就是「逐行」的样子。
但结尾那句话要记住:这个需求用 CASE 一句就能算完。游标在这里是「能用」,不是「必须用」。
练手可以,上生产该换掉。
30 秒实验
- 把
while True改成while cur.fetchone() is not None,看会发生什么。 每一行都被吃掉两遍——判断和取值写在一起,第一行就被消耗掉了。 这就是「FETCH 要写两次」的原因。 - 把
rules.get(tid, 1.0)改成rules[tid],跑一次。 3 类之外还有别的类时直接报KeyError——游标循环里出异常,整个批就停了。 - 用一条
CASE语句写出同样的结果,跟逐行版本逐行对比。 结果应该完全一样,但速度快几十倍——这就是「集合 vs 逐行」的直观感受。
常见误区
- 「
CLOSE和DEALLOCATE不写也行,反正脚本跑完就没了」。批跑完了可能释放, 但如果是在存储过程里、或者连接池复用连接,锁和内存会一直挂着。 - 「
FETCH写一次就够了」。它必须在循环前后各出现一次。 少一次会漏第一行,多一次会漏最后一行。 - 「每行算法不一样就必须用游标」。
CASE能表达的差异不算差异。 只有「算不出来」的活才需要游标。
自测三问(点开对答案)
1. 游标的五个动作分别是什么? —— **`DECLARE` 定义、`OPEN` 打开、`FETCH` 取一行、`CLOSE` 关闭、`DEALLOCATE` 释放。** **后两个最容易被漏,而且漏了不报错。** 2. 为什么 `FETCH` 要写两次? —— **因为要先取第一行才能进循环判断。** 循环前取一次是为了「有行可处理」, **循环末尾取一次是为了「换下一行」——只写一次一定会漏掉一头。** 3. `@@FETCH_STATUS` 的三个值分别是什么意思? —— **0 取到行了、-1 越界(没更多行)、-2 这一行已经被删了。** **循环条件是 `= 0`。**下一篇 13.3 讲怎么「看」游标:sp_describe_cursor、sp_describe_cursor_columns、
sp_describe_cursor_tables 三个系统过程各给一段信息——排查「表被锁住」时特别有用。
- ☐ (本篇完,下一篇 13.3 使用系统过程查看游标)