FETCH / MOVE / CLOSE
游标打开之后,真正的取数工作由三条语句完成:FETCH 取行、MOVE 移动位置、CLOSE 关闭门户。它们共享同一套「方向(direction)」语法,但副作用完全不同——FETCH 会改变当前行并把数据交给你,MOVE 只移动位置不返回数据,CLOSE 则彻底销毁门户。本节要讲清 PL/pgSQL 里这三条语句的精确可用方向集,以及一个最容易出错的事实:PL/pgSQL 的 FETCH 与 SQL 层的 FETCH 支持的写法并不相同。
本节的核心陷阱
网上大量教程会写 FETCH ALL FROM cur 或 FETCH FORWARD 10 FROM cur 用在 PL/pgSQL 里。这些是 SQL 层 的写法。在 PL/pgSQL 中,FETCH 的方向集被削减了,多余写法会直接报语法错误。本节会逐项列出允许的写法。
前置知识
阅读本节前,建议先了解:游标声明与打开
基础概念
先厘清三条语句各自的职责边界。
| 语句 | 是否返回数据 | 是否移动位置 | 是否销毁门户 | 典型用途 |
|---|---|---|---|---|
FETCH | 是 | 是 | 否 | 逐行读取 |
MOVE | 否 | 是 | 否 | 跳行、定位、计数 |
CLOSE | 否 | — | 是 | 释放资源 |
三者的共同点:都接受一个方向(direction)参数,用来描述「相对当前位置如何移动」。
结果集(行流)
▸ row1 row2 row3 row4 row5
↑
当前行
FETCH NEXT → 读 row3,当前位置移到 row3
MOVE NEXT → 不读数据,当前位置移到 row3
FETCH PRIOR → 读 row2,当前位置移到 row2(需 SCROLL)
CLOSE → 门户销毁位置模型
理解 FETCH / MOVE 的关键是「当前位置」这个隐含状态。它由门户持有,语义如下:
| 位置状态 | 含义 |
|---|---|
| 在首行之前 | OPEN 之后的初始状态 |
| 在某一行上 | 最近一次成功 FETCH / MOVE 停下的位置 |
| 在末行之后 | 已越过最后一行(FETCH 返回 0 行) |
FETCH 的动作用一句话描述:先把位置往指定方向移动,再返回新位置所在的行。
FOUND 与空结果
每次 FETCH 都会刷新内置变量 FOUND:
| 情况 | FOUND | 返回行数 |
|---|---|---|
| 取到一行 | true | 1 |
| 再也取不到 | false | 0 |
MOVE 也会设置 FOUND,含义是「是否有行被移动经过」。
| 语句 | FOUND = true 的含义 |
|---|---|
FETCH NEXT | 确实取到了一行 |
MOVE NEXT | 确实移动到了一行 |
MOVE 5 | 至少移动了一行(未越界) |
CLOSE | 不改变 FOUND |
GET DIAGNOSTICS 拿到行数
FETCH / MOVE 到底影响了几行,可以用 GET DIAGNOSTICS 读取:
GET DIAGNOSTICS 变量 = ROW_COUNT;| 语句 | ROW_COUNT 的含义 |
|---|---|
FETCH | 返回的行数(0 或 1,多行形式可 >1) |
MOVE | 被移动经过的行数 |
这是实现「跳过 N 行」或「批量取 N 行」时判断是否到结尾的可靠手段。
方向(direction)的完整集合
SQL 层 FETCH 支持的方向非常丰富:
| 方向 | 含义 | 读取行数 |
|---|---|---|
NEXT | 下一行(默认) | 1 |
PRIOR | 上一行 | 1 |
FIRST | 第一行 | 1 |
LAST | 最后一行 | 1 |
ABSOLUTE n | 第 n 行 | 1 |
RELATIVE n | 相对当前位置偏移 n | 1 |
n | 等同于 FORWARD n | n |
ALL | 全部剩余行 | 多 |
FORWARD | 下一行 | 1 |
FORWARD n | 向后 n 行 | n |
FORWARD ALL | 全部剩余行 | 多 |
BACKWARD | 上一行 | 1 |
BACKWARD n | 向前 n 行 | n |
BACKWARD ALL | 全部已过行 | 多 |
但 PL/pgSQL 不是 SQL 层。 PL/pgSQL 的 FETCH 只允许下面这个子集:
| PL/pgSQL 允许的方向 | 是否允许 |
|---|---|
NEXT | ✅ |
PRIOR | ✅ |
FIRST | ✅ |
LAST | ✅ |
ABSOLUTE count | ✅ |
RELATIVE count | ✅ |
FORWARD | ✅(仅一行) |
BACKWARD | ✅(仅一行) |
裸 count | ❌ 不支持 |
ALL | ❌ 不支持 |
FORWARD count / FORWARD ALL | ❌ 不支持 |
BACKWARD count / BACKWARD ALL | ❌ 不支持 |
这一点必须记牢:PL/pgSQL 的 FETCH 与 MOVE 方向集不同。MOVE 反而支持多行与 ALL。
| 方向写法 | PL/pgSQL FETCH | PL/pgSQL MOVE |
|---|---|---|
NEXT / PRIOR / FIRST / LAST | ✅ | ✅ |
ABSOLUTE n / RELATIVE n | ✅ | ✅ |
FORWARD / BACKWARD(单行) | ✅ | ✅ |
FORWARD n / BACKWARD n | ❌ | ✅ |
ALL / FORWARD ALL | ❌ | ✅ |
裸 n | ❌(已废弃写法) | ⚠️ 可用但已废弃,建议写 FORWARD n |
语法与用法
FETCH 语法原型
FETCH [ 方向 { FROM | IN } ] 游标 INTO 目标;方向 可省略,省略时等价于 NEXT。
FETCH 游标 INTO 目标; -- 等价于 FETCH NEXT
FETCH NEXT 游标 INTO 目标; -- 等价(FROM/IN 可省)
FETCH NEXT FROM 游标 INTO 目标; -- 显式 FROM
FETCH PRIOR FROM 游标 INTO 目标;
FETCH FIRST FROM 游标 INTO 目标;
FETCH LAST FROM 游标 INTO 目标;
FETCH ABSOLUTE 3 FROM 游标 INTO 目标;
FETCH RELATIVE -2 FROM 游标 INTO 目标;
FETCH FORWARD FROM 游标 INTO 目标; -- 单行
FETCH BACKWARD FROM 游标 INTO 目标; -- 单行目标 可以是:
| 目标形式 | 示例 | 说明 |
|---|---|---|
record 变量 | INTO r | 最常用 |
row 变量 | INTO r | 表行类型 |
| 标量变量列表 | INTO a, b, c | 列数与变量数须一致 |
最小可运行示例
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
FETCH NEXT FROM cur INTO r;
RAISE NOTICE '第一行: % - %', r.id, r.name;
FETCH NEXT FROM cur INTO r;
RAISE NOTICE '第二行: % - %', r.id, r.name;
CLOSE cur;
END;
$$;NOTICE: 第一行: 1 - 机械键盘
NOTICE: 第二行: 2 - 显示器
DO逐列取值
不一定要用 record,也可以直接取到标量变量:
DO $$
DECLARE
cur CURSOR FOR SELECT id, name, price FROM products ORDER BY id;
v_id int;
v_name text;
v_price numeric;
BEGIN
OPEN cur;
FETCH NEXT FROM cur INTO v_id, v_name, v_price;
RAISE NOTICE '% 号商品 % 售价 %', v_id, v_name, v_price;
CLOSE cur;
END;
$$;NOTICE: 1 号商品 机械键盘 售价 599.00
DO变量的数量与顺序必须与查询的列一一对应,否则报错。
MOVE 语法原型
MOVE [ 方向 { FROM | IN } ] 游标;MOVE 没有 INTO——它不返回数据。
MOVE NEXT FROM cur; -- 前进一行
MOVE 10 FROM cur; -- 前进 10 行(PL/pgSQL 中已废弃写法)
MOVE FORWARD 10 FROM cur; -- 前进 10 行(推荐写法)
MOVE ALL FROM cur; -- 移动到末尾
MOVE ABSOLUTE 5 FROM cur; -- 跳到第 5 行最小可运行示例:MOVE
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
n int;
BEGIN
OPEN cur;
MOVE FORWARD 1 FROM cur; -- 跳过第一行
GET DIAGNOSTICS n = ROW_COUNT;
RAISE NOTICE '跳过了 % 行', n;
FETCH NEXT FROM cur INTO r;
RAISE NOTICE '当前行: % - %', r.id, r.name;
CLOSE cur;
END;
$$;NOTICE: 跳过了 1 行
NOTICE: 当前行: 2 - 显示器
DOCLOSE 语法原型
PL/pgSQL 里关闭单个游标:
CLOSE 游标变量;SQL 层可以关一个或全部:
CLOSE 游标名;
CLOSE ALL;| 形式 | 层 | 说明 |
|---|---|---|
CLOSE 变量; | PL/pgSQL | 按变量持有的门户名关闭 |
CLOSE 名字; | SQL | 按门户名关闭 |
CLOSE ALL; | SQL | 关闭当前会话所有门户 |
关闭未打开的游标会报错
-- 错误!关闭一个尚未打开的绑定游标
DO $$
DECLARE
cur CURSOR FOR SELECT 1;
BEGIN
CLOSE cur; -- ERROR: cursor "cur" is not open
END;
$$;ERROR: cursor "cur" is not open在 PG 16 之前,绑定游标变量初始值就是变量名,这个错误报的是 cursor "cur" is not open;现在语义更明确。
重复关闭同一个游标同样报错:
-- 错误!重复关闭
DO $$
DECLARE
cur refcursor;
BEGIN
OPEN cur FOR SELECT 1;
CLOSE cur;
CLOSE cur; -- ERROR: cursor "..." is not open
END;
$$;ERROR: cursor "<unnamed portal 2>" is not open详细说明
FETCH 的一个常见误解:多行形式在 PL/pgSQL 里不存在
这是最值得单独强调的一点。很多从 SQL 层 FETCH 迁移过来的写法会全部失败:
-- 错误!PL/pgSQL 不支持这些方向
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products;
r record;
BEGIN
OPEN cur;
FETCH ALL FROM cur INTO r; -- 语法错误
FETCH FORWARD 10 FROM cur INTO r; -- 语法错误
FETCH 10 FROM cur INTO r; -- 语法错误
END;
$$;ERROR: syntax error at or near "ALL"
LINE 7: FETCH ALL FROM cur INTO r;正确写法是显式循环:
-- 推荐:用循环代替 FETCH ALL
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
LOOP
FETCH NEXT FROM cur INTO r;
EXIT WHEN NOT FOUND;
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
CLOSE cur;
END;
$$;NOTICE: 1 - 机械键盘
NOTICE: 2 - 显示器
NOTICE: 3 - 鼠标垫
DO这条「循环 + EXIT WHEN NOT FOUND」正是 PL/pgSQL 游标处理的标准范式。
为什么 FETCH 方向集被削减
原因在于 PL/pgSQL 的 FETCH 必须落到「目标(INTO 目标)」上,而目标只能容纳固定数量的变量。多行形式(ALL、FORWARD n)返回的行数在编译期未知,无法映射到静态的目标列表,所以被移出语法。MOVE 不涉及目标,因此可以保留多行形式。
| 语句 | 有 INTO 目标 | 是否可以多行 |
|---|---|---|
PL/pgSQL FETCH | 是 | 否 |
PL/pgSQL MOVE | 否 | 是 |
ABSOLUTE 与 RELATIVE 的语义
ABSOLUTE n 与 RELATIVE n 是两个与「当前位置」关系不同的定位方式。
| 写法 | 含义 | 边界行为 |
|---|---|---|
ABSOLUTE n(n > 0) | 从开头数第 n 行 | 超出末行 → 定位到末行之后 |
ABSOLUTE n(n < 0) | 从末尾倒数第 |n| 行 | 需 SCROLL |
ABSOLUTE 0 | 回到首行之前 | — |
RELATIVE n(n > 0) | 相对当前位置向后 n 行 | — |
RELATIVE n(n < 0) | 相对当前位置向前 |n| 行 | 需 SCROLL |
RELATIVE 0 | 重读当前行 | 需 SCROLL(或重读一次) |
DO $$
DECLARE
cur SCROLL CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
FETCH ABSOLUTE 2 FROM cur INTO r;
RAISE NOTICE 'ABSOLUTE 2 -> %', r.name;
FETCH ABSOLUTE -1 FROM cur INTO r;
RAISE NOTICE 'ABSOLUTE -1 -> %', r.name;
FETCH RELATIVE -2 FROM cur INTO r;
RAISE NOTICE 'RELATIVE -2 -> %', r.name;
CLOSE cur;
END;
$$;NOTICE: ABSOLUTE 2 -> 显示器
NOTICE: ABSOLUTE -1 -> 鼠标垫
NOTICE: RELATIVE -2 -> 机械键盘
DO负号方向(向前)与 RELATIVE 0 都要求游标可向后读,也就是必须是 SCROLL 的。
-- 错误!非 SCROLL 游标做向后读
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id; -- 默认,不可向后
r record;
BEGIN
OPEN cur;
FETCH NEXT FROM cur INTO r;
FETCH PRIOR FROM cur INTO r; -- 可能失败
END;
$$;ERROR: cursor can only scan forward
HINT: Declare it with SCROLL option to enable backward scan.注意「是否报错」取决于查询能否支持向后扫描:默认(不写 SCROLL / NO SCROLL)时,若查询本身可支持反向,则允许;否则报上述错误。要保证可向后读,就显式声明 SCROLL。
向后读失败的两种形态
| 情况 | 结果 |
|---|---|
显式 NO SCROLL + 向后读 | 报错 55000(object_not_in_prerequisite_state 类)或计划错误 |
| 默认声明 + 查询不支持反向 | 报 cursor can only scan forward |
显式 SCROLL | 一定可用(代价是物化) |
官方文档的措辞是:向后 FETCH 很可能失败,除非游标在声明或打开时指定了 SCROLL。所以「默认可以向后读」只是偶然,不要依赖。
FETCH 与 FOR UPDATE 配合更新行
游标配合 WHERE CURRENT OF 可以更新「当前行」:
DO $$
DECLARE
cur CURSOR FOR
SELECT id, price FROM products WHERE price < 500 FOR UPDATE;
r record;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO r;
EXIT WHEN NOT FOUND;
UPDATE products SET price = price * 1.1 WHERE CURRENT OF cur;
RAISE NOTICE '调价: % -> %', r.id, r.price * 1.1;
END LOOP;
CLOSE cur;
END;
$$;NOTICE: 调价: 1 -> 658.9
NOTICE: 调价: 3 -> 328.9
DO要点:
| 要求 | 说明 |
|---|---|
游标查询须带 FOR UPDATE / FOR SHARE | 否则 WHERE CURRENT OF 报错 |
| 查询不能有分组、聚合、去重等 | 行无法与基表行对应 |
| 若查询涉及多表连接 | WHERE CURRENT OF 只更新游标基表 |
游标不能是 SCROLL | SCROLL 与 FOR UPDATE 互斥 |
MOVE 的多行形式与计数
MOVE 的多行形式正是「跳过若干行」的利器:
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id;
r record;
n int;
BEGIN
OPEN cur;
MOVE FORWARD 2 FROM cur;
GET DIAGNOSTICS n = ROW_COUNT;
RAISE NOTICE '前进了 % 行', n;
FETCH NEXT FROM cur INTO r;
RAISE NOTICE '现在在 id = %', r.id;
MOVE ALL FROM cur; -- 直接跳到末尾
GET DIAGNOSTICS n = ROW_COUNT;
RAISE NOTICE '剩余 % 行被跳过', n;
CLOSE cur;
END;
$$;NOTICE: 前进了 2 行
NOTICE: 现在在 id = 3
NOTICE: 剩余 0 行被跳过
DO越过末尾后 MOVE 不报错,只是 ROW_COUNT 小于请求的行数——这种「软越界」让它很适合分页实现。
MOVE 到越界的返回值
| 请求 | 结果集剩余 | ROW_COUNT | FOUND |
|---|---|---|---|
MOVE FORWARD 5 | 3 行 | 3 | true(移动了行) |
MOVE FORWARD 5 | 0 行 | 0 | false |
MOVE ALL | 4 行 | 4 | true |
注意 MOVE 越界不会跳过错误,它只是「尽可能移动」。
CLOSE 的两种回收路径
| 路径 | 触发 | 是否可再次 FETCH |
|---|---|---|
显式 CLOSE | 你调用 CLOSE | 否,报 cursor ... is not open |
| 隐式关闭 | 事务结束 | 否,门户不存在 |
在循环里要特别注意配对:每次 OPEN 都要有一个对应的 CLOSE。
-- 推荐:每次 OPEN 都配对 CLOSE
FOR i IN 1..3 LOOP
OPEN cur FOR SELECT * FROM products WHERE id = i;
FETCH cur INTO r;
CLOSE cur;
END LOOP;游标已被关闭时的表现
-- 错误!对已关闭游标 FETCH
DO $$
DECLARE
cur refcursor;
r record;
BEGIN
OPEN cur FOR SELECT 1;
CLOSE cur;
FETCH cur INTO r; -- ERROR
END;
$$;ERROR: cursor "<unnamed portal 1>" is not open对应的 SQLSTATE 是 34000(invalid_cursor_name 类)或 24000(invalid_cursor_state),具体取决于是「名字不存在」还是「状态不对」。
部分取数:分批返回避免内存爆掉
游标最大的实用价值是「分批处理」。下面这个函数每次只取 batch_size 行:
CREATE OR REPLACE FUNCTION process_in_batches(p_batch int)
RETURNS int AS $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id;
r record;
total int := 0;
skipped int;
BEGIN
OPEN cur;
LOOP
MOVE FORWARD p_batch FROM cur;
GET DIAGNOSTICS skipped = ROW_COUNT;
EXIT WHEN skipped = 0;
total := total + skipped;
RAISE NOTICE '已处理 % 行', total;
END LOOP;
CLOSE cur;
RETURN total;
END;
$$ LANGUAGE plpgsql;
SELECT process_in_batches(1);NOTICE: 已处理 1 行
NOTICE: 已处理 2 行
NOTICE: 已处理 3 行
process_in_batches
--------------------
3
(1 row)游标在 EXIT 之前的清理
因为 EXCEPTION 块会回滚整块内的变更,所以「游标在异常块里没关掉」通常不会造成持久问题——门户会随子事务一起释放。但在正常的 BEGIN ... END 流程里,提前 EXIT 而忘记 CLOSE 会让门户留到事务结束。
-- 不推荐:提前 EXIT,未关闭
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO r;
EXIT WHEN NOT FOUND;
EXIT WHEN r.id = 2; -- 提前退出,cur 未关
END LOOP;
-- cur 仍然打开
END;
$$;-- 推荐:无论怎样都关闭
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
BEGIN
LOOP
FETCH cur INTO r;
EXIT WHEN NOT FOUND;
EXIT WHEN r.id = 2;
END LOOP;
EXCEPTION WHEN OTHERS THEN
CLOSE cur; -- 异常路径也关掉
RAISE;
END;
CLOSE cur;
END;
$$;实战示例
示例一:完整遍历并输出每一行
需求:打印商品列表,每行一个。
DO $$
DECLARE
cur CURSOR FOR SELECT id, name, price FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
LOOP
FETCH NEXT FROM cur INTO r;
EXIT WHEN NOT FOUND;
RAISE NOTICE '% | % | %', r.id, r.name, r.price;
END LOOP;
CLOSE cur;
END;
$$;NOTICE: 1 | 机械键盘 | 599.00
NOTICE: 2 | 显示器 | 1299.00
NOTICE: 3 | 鼠标垫 | 299.00
DO示例二:跳过前 N 行再取
需求:分页取第 2 页(每页 2 行),即跳过前 2 行后取 2 行。
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
skip int := 2;
take int := 2;
moved int;
got int := 0;
BEGIN
OPEN cur;
MOVE FORWARD skip FROM cur;
GET DIAGNOSTICS moved = ROW_COUNT;
RAISE NOTICE '跳过 % 行', moved;
LOOP
EXIT WHEN got >= take;
FETCH NEXT FROM cur INTO r;
EXIT WHEN NOT FOUND;
got := got + 1;
RAISE NOTICE '第 % 条: %', got, r.name;
END LOOP;
CLOSE cur;
END;
$$;NOTICE: 跳过 2 行
NOTICE: 第 1 条: 鼠标垫
DO示例三:用 WHERE CURRENT OF 批量调价
需求:把所有低于 500 元的商品价格上调 10%。
DO $$
DECLARE
cur CURSOR FOR
SELECT id, price FROM products WHERE price < 500 FOR UPDATE;
r record;
cnt int := 0;
BEGIN
OPEN cur;
LOOP
FETCH NEXT FROM cur INTO r;
EXIT WHEN NOT FOUND;
UPDATE products SET price = price * 1.10 WHERE CURRENT OF cur;
cnt := cnt + 1;
END LOOP;
CLOSE cur;
RAISE NOTICE '共调整 % 条', cnt;
END;
$$;NOTICE: 共调整 2 条
DO示例四:统计到结尾的行数
需求:用 MOVE ALL 快速得到剩余行数。
DO $$
DECLARE
cur CURSOR FOR SELECT id FROM products ORDER BY id;
r record;
n int;
total int := 0;
BEGIN
OPEN cur;
FETCH NEXT FROM cur INTO r; -- 先取一行
total := 1;
MOVE ALL FROM cur;
GET DIAGNOSTICS n = ROW_COUNT;
total := total + n;
RAISE NOTICE '一共 % 行', total;
CLOSE cur;
END;
$$;NOTICE: 一共 3 行
DO示例五:SCROLL 游标前后翻页
需求:模拟一个「上一页 / 下一页」的浏览行为。
DO $$
DECLARE
cur SCROLL CURSOR FOR SELECT id, name FROM products ORDER BY id;
r record;
BEGIN
OPEN cur;
FETCH FIRST FROM cur INTO r;
RAISE NOTICE '首页: %', r.name;
FETCH NEXT FROM cur INTO r;
RAISE NOTICE '下一页: %', r.name;
FETCH PRIOR FROM cur INTO r;
RAISE NOTICE '回上一页: %', r.name;
FETCH LAST FROM cur INTO r;
RAISE NOTICE '末页: %', r.name;
CLOSE cur;
END;
$$;NOTICE: 首页: 机械键盘
NOTICE: 下一页: 显示器
NOTICE: 回上一页: 机械键盘
NOTICE: 末页: 鼠标垫
DO注意事项
- PL/pgSQL
FETCH不支持多行方向:ALL、FORWARD n、裸n都会报语法错误。 - PL/pgSQL
MOVE支持多行方向:FORWARD n、BACKWARD n、ALL都可用。 FETCH必须有INTO目标,MOVE不能有。- 向后读要求
SCROLL,否则可能报cursor can only scan forward。 WHERE CURRENT OF要求游标查询带FOR UPDATE,且游标不能是SCROLL。CLOSE一个未打开/已关闭的游标会报错。- 每行命令后
FOUND会被刷新,注意在循环内别被其他语句覆盖。 MOVE越界不报错,用ROW_COUNT判断实际情况。
| 错误写法 | 报错/后果 | 正确做法 |
|---|---|---|
FETCH ALL FROM cur INTO r | 语法错误 | 循环 + EXIT WHEN NOT FOUND |
FETCH FORWARD 10 FROM cur INTO r | 语法错误 | 用 MOVE FORWARD 10 或循环 10 次 FETCH |
非 SCROLL 游标 FETCH PRIOR | cursor can only scan forward | 声明为 SCROLL |
WHERE CURRENT OF 但无 FOR UPDATE | 报错 | 游标查询加 FOR UPDATE |
重复 CLOSE cur | cursor ... is not open | 只在打开后关闭一次 |
用 MOVE 后忘记 FOUND 是「移动」含义 | 逻辑错乱 | 用 ROW_COUNT 判断 |
最佳实践
- 遍历统一用「
LOOP+FETCH NEXT+EXIT WHEN NOT FOUND」,这是最不易出错的范式。 MOVE只用来「跳」和「计数」,需要数据就用FETCH。- 判结尾优先用
FOUND,判数量用GET DIAGNOSTICS ... ROW_COUNT。 - 需要向后读时显式声明
SCROLL,不要赌默认行为。 OPEN/CLOSE成对出现,尤其在循环与提前EXIT场景。- 大批量数据处理优先考虑
FOR ... IN隐式游标,它自动开关、无需手写FETCH。
| 场景 | 推荐 | 反例 |
|---|---|---|
| 遍历全部行 | LOOP + FETCH NEXT | FETCH ALL(PL/pgSQL 不支持) |
| 跳过 N 行 | MOVE FORWARD N + ROW_COUNT | 循环 FETCH N 次丢弃 |
| 判断是否取完 | EXIT WHEN NOT FOUND | 靠 id 是否为空判断 |
| 更新当前行 | WHERE CURRENT OF + FOR UPDATE | 用主键再查一次 |
| 向前读 | SCROLL 游标 | 默认游标赌运气 |
-- 推荐:标准遍历范式
OPEN cur;
LOOP
FETCH NEXT FROM cur INTO r;
EXIT WHEN NOT FOUND;
-- 处理 r
END LOOP;
CLOSE cur;
-- 不推荐:依赖 SQL 层写法
FETCH ALL FROM cur INTO r; -- PL/pgSQL 会报语法错误下一节
下一节我们学习:隐式游标(FOR IN),看看 PL/pgSQL 如何用 FOR ... IN 自动完成「打开 → 逐行取 → 关闭」,什么时候该用它替代显式游标。