游标把「一次一批」拆成「一次一行」:打开结果集,指针一行一行往下挪。
实测 1 万行打九折:游标逐行改比一条 UPDATE 慢约 5 倍——而且这个倍数只在本地内存里这么小。
只有当「处理逻辑不在数据库里」或「行与行之间有顺序依赖」时,游标才是必需的。
前面十章的查询全是「集合」思维:一条 SQL 处理一批行,
你只说「要什么」,不管「怎么一行行来」。
游标是反过来的: 结果集摆在那儿,你一行一行取、一行一行处理。
教材 13.1 分三小节:游标的实现、游标的类型、什么时候真的需要游标。
最后那一小节最值钱——它决定了你以后会不会滥用这个东西。
SQLite 没有 T-SQL 的游标对象,
所以演示里逐行处理用 Python 的 cursor 复刻(它本来就是「结果集 + 当前位置」),
而「慢多少」那一段是真测的。
一、从「一次一批」到「一次一行」
需求:给每个商品按类别打折。 用集合语句,一条 UPDATE 就完了:SET price = price * 折扣。
用游标,得这么走:
DECLARE cur_goods CURSOR FOR
SELECT id, gname, type_id, price FROM tb_goods ORDER BY id
OPEN cur_goods
FETCH NEXT FROM cur_goods -- 一行一行取,取完为止
CLOSE cur_goods
DEALLOCATE cur_goods
DECLARE 定义、OPEN 打开、FETCH 取一行、CLOSE 关掉、DEALLOCATE 释放。五步,
一步都不能省——13.2 会细讲每一步漏了会怎样。
游标不改变结果,只改变「怎么拿」。 同一个 SELECT,用游标取回来和直接查出来是同一批行,
区别在于你能不能「停在中间」。
二、游标慢多少:一万行实测
| 1 万行打九折 | 最快一次 |
|---|---|
| 游标逐行改 | 66.2 ms |
一条 UPDATE 改完 |
14.0 ms |
差约 5 倍。 账很好算:游标每行都要「取回来、在客户端算、再发一条 UPDATE 过去」,
一万行就是一万次往返;集合语句只有一次。
但要注意口径: 这里只差 5 倍,是因为客户端和数据库跑在同一台机器的内存里。真走网络时,
一万行就是一万次网络往返——差两个数量级都不奇怪。
SQL Server 的游标跑在服务端,往返换成了解析和执行的开销,
所以差距没有网络场景那么夸张,但道理一样:逐行永远比集合贵。
所以第一条原则是:能用集合语句解决的,别用游标。这不是风格问题,
是「每行一次」的账——行数越多,账越难看。
三、什么时候真的需要游标
| 场景 | 为什么集合语句干不了 |
|---|---|
| 逐行调外部接口 | SQL 里没法发 HTTP 请求,只能取一行、发一次 |
| 逐行生成文件或报表 | 每行格式不同、要按上一行的状态决定下一行怎么写 |
| 分批重算,每批依赖上一批结果 | 集合语句一次算完,拿不到中间状态 |
| 逐行执行动态 SQL | 要拼出来的语句每行都不一样 |
共同点就两条:一是「处理逻辑不在数据库里」,二是「行与行之间有顺序依赖」。满足其中一条,
游标就有了正当理由。
反过来说,凡是「每行独立、规则一样」的活,都该用集合语句——
包括看起来很像逐行的「累加」。 累计求和、环比、排名,现在的 SQL 都有窗口函数能算(9.3 讲过)。
还有一类特别容易误判:「逐行判断然后插入到另一张表」。这个用 INSERT ... SELECT 一句就完了,
根本不需要游标。
判断标准很简单:这段逻辑写成一条 SQL 会很长但写得出来吗?写得出来就别用游标。
四、游标的四种类型
| 类型 | 能不能前后移动 | 看得到别人的修改吗 |
|---|---|---|
静态(STATIC) |
能 | 看不到,用的是打开时的快照 |
动态(DYNAMIC) |
能 | 能看到,包括新插入的行 |
只进(FORWARD_ONLY) |
只能往下 | 部分能看到 |
键集驱动(KEYSET) |
能 | 能看到修改,看不到新插入 |
不写类型时,SQL Server 默认给 FORWARD_ONLY——最快、最省资源,
也是绝大多数场景该用的。 只往下走,不需要维护「我在第几行」的额外状态。
越「能看见别人改的」,代价越大。 动态游标要在每次 FETCH 时重新检查底层数据,
数据量大时慢得离谱;静态游标要先做一份快照,内存和临时库都要占。
实际写代码时的经验: 九成场景用 FORWARD_ONLY + READ_ONLY——
只读、只往下,最省。 需要改数据就另开一条 UPDATE,别指望游标边读边改。
五、动手 Demo:逐行处理的代价:一万行,5 倍
这一段先看游标的五步长什么样,再在 1 万行上真测一次「逐行改」和「一条 UPDATE」的差别。
然后列出「什么时候真的需要游标」,最后过一遍四种类型的取舍。
import sqlite3, time
c = sqlite3.connect(u":memory:", isolation_level=None)
c.execute(u"CREATE TABLE t (id INTEGER PRIMARY KEY, v REAL)")
def by_cursor():
cur = c.cursor()
cur.execute(u"SELECT id, v FROM t")
while True:
row = cur.fetchone()
if row is None:
break
c.execute(u"UPDATE t SET v = ? WHERE id = ?", (row[1] * 0.9, row[0]))
cur.close()
def by_set():
c.execute(u"UPDATE t SET v = v * 0.9")
print(u"逐行 %.1f ms,集合 %.1f ms" % (clock(by_cursor), clock(by_set)))
实跑输出:
--------------------------------------------------------------------
游标是什么:一行一行地处理
--------------------------------------------------------------------
① 从「一次一批」到「一次一行」
--------------------------------------------------------------------
前面所有查询都是「集合」思维:一条 SQL 处理一批行,你只管说要什么。
游标是反过来的:把结果集打开,像指针一样一行一行往下挪,每挪一行处理一行。
演示里给每个商品按类别打折——如果用游标,是这么走的:
DECLARE cur_goods CURSOR FOR
SELECT id, gname, type_id, price FROM tb_goods ORDER BY id
OPEN cur_goods
FETCH NEXT FROM cur_goods -- 一行一行取,取完为止
CLOSE cur_goods
DEALLOCATE cur_goods
同样的活,用一条集合语句也能干:UPDATE ... SET price = price * 折扣。
那为什么还要游标?因为有些活集合语句真的干不了——比如「每行都去调一次外部接口」。
② 游标慢多少:一万行实测
--------------------------------------------------------------------
| 1 万行打九折 | 最快一次(毫秒) |
|---|---:|
| 游标逐行改 | 66.0 |
| 一条 UPDATE 改完 | 14.2 |
差 5 倍。这里量的是「逐行处理」这件事本身——
每行都要:取回来、在客户端算、再发一条 UPDATE 过去。一万行就是一万次往返。
SQL Server 的游标也是这个道理,只不过它跑在服务端,往返换成了解析和执行的开销。
! 所以第一条原则是:能用集合语句解决的,别用游标。这不是风格问题,是「每行一次往返」的账。
注意口径:这里差 5 倍,看着不算夸张——因为客户端和库在同一台机器的内存里。
真走网络时,一万行就是一万次网络往返,差两个数量级都不奇怪。这才是游标最大的成本。
③ 那什么时候真的需要游标
--------------------------------------------------------------------
| 场景 | 为什么集合语句干不了 |
|---|---|
| 逐行调外部接口 | SQL 里没法发 HTTP 请求,只能取一行、发一次 |
| 逐行生成文件或报表 | 每行格式不同、要按上一行的状态决定下一行怎么写 |
| 分批重算,每批依赖上一批结果 | 集合语句一次算完,拿不到中间状态 |
| 逐行执行动态 SQL | 要拼出来的语句每行都不一样 |
共同点:处理逻辑不在数据库里,或者行与行之间有顺序依赖。
反过来说,凡是「每行独立、规则一样」的活,都该用集合语句——包括看起来很像逐行的「累加」。
④ 游标的四种类型
--------------------------------------------------------------------
| 类型 | 能不能前后移动 | 看得到别人的修改吗 |
|---|---|---|
| 静态(STATIC) | 能 | 看不到,用的是打开时的快照 |
| 动态(DYNAMIC) | 能 | 能看到,包括新插入的行 |
| 只进(FORWARD_ONLY) | 只能往下 | 部分能看到 |
| 键集驱动(KEYSET) | 能 | 能看到修改,看不到新插入 |
不写类型时,SQL Server 默认给 FORWARD_ONLY——最快、最省资源,也是绝大多数场景该用的。
越「能看见别人改的」,代价越大:动态游标要在每次 FETCH 时重新检查,慢得多。
66.2 对 14.0,差约 5 倍。 注意这两个数都只测了「改」的过程,
没算把整批数据取回来的时间——都取回来的话,差距会更大。
为什么集合快? 因为它只发一条语句,数据库内部一次扫完、一次写日志。
游标是「取一行、发一条、取一行、发一条」,每条 UPDATE 都要单独解析、单独记账。
「5 倍」这个数别当结论记住,要当「口径」记住。 在本地内存库上是 5 倍,
在走网络的客户端上可能是 500 倍。 换了环境就得重测。
所以真正该记的是那句原则: 能用集合语句解决的,别用游标。
游标只在「SQL 表达不出来」的时候才有资格上场。
30 秒实验
- 把 1 万行改成 1000 行,重测。两个数的倍数会变小——因为网络/调用开销被摊薄得少了。
- 把逐行那段里的
UPDATE换成SELECT(只读不写),重测。 差距会小很多——说明慢的主要是「每条语句都要写一次」这笔账。 - 想一想:给一张十万行的表「按上个月的销量给每个客户的等级重新打标」,该用游标吗?
不该——用一条带
CASE的UPDATE就够了。销量是算出来的,不需要逐行。
常见误区
- 「游标就是循环,循环比 SQL 灵活」。灵活是真的,
代价也是真的:1 万行慢 5 倍,走网络能差两个数量级。
能一句
UPDATE干完的,就别写游标。 - 「游标能边读边改同一张表」。很多数据库会因此报错或者加锁加到死。 要改就另开一条语句,别在游标里改它自己读的表。
- 「游标慢是因为客户端慢」。服务端游标一样慢——慢在「逐行」这件事本身, 因为每行都要单独定位、单独写日志。
自测三问(点开对答案)
1. 游标和集合语句最根本的区别是什么? —— **一次处理几行。** 集合语句一次处理一批,你只说「要什么」; **游标把结果集打开,一行一行往下挪,每行单独处理。** 2. 1 万行逐行改,实测慢多少倍?这个数该怎么理解? —— **约 5 倍**——但这是「客户端和库在同一台机器内存里」的口径。 **走网络时一万行就是一万次往返,差两个数量级都不奇怪。** 3. 什么样的活必须用游标? —— **处理逻辑不在数据库里(比如逐行调外部接口),或者行与行之间有顺序依赖** (后一行要按前一行的结果算)。**每行独立、规则一样的活,都该用集合语句。**下一篇 13.2 讲游标的五个动作:
DECLARE、OPEN、FETCH、CLOSE、DEALLOCATE——重点是后两个,漏了不报错,
但锁和内存会一直挂着。
- ☐ (本篇完,下一篇 13.2 游标的基本操作)