Skip to content

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)参数,用来描述「相对当前位置如何移动」。

text
结果集(行流)
  ▸ 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返回行数
取到一行true1
再也取不到false0

MOVE 也会设置 FOUND,含义是「是否有行被移动经过」。

语句FOUND = true 的含义
FETCH NEXT确实取到了一行
MOVE NEXT确实移动到了一行
MOVE 5至少移动了一行(未越界)
CLOSE不改变 FOUND

GET DIAGNOSTICS 拿到行数 ​

FETCH / MOVE 到底影响了几行,可以用 GET DIAGNOSTICS 读取:

sql
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相对当前位置偏移 n1
n等同于 FORWARD nn
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 FETCHPL/pgSQL MOVE
NEXT / PRIOR / FIRST / LAST✅✅
ABSOLUTE n / RELATIVE n✅✅
FORWARD / BACKWARD(单行)✅✅
FORWARD n / BACKWARD n❌✅
ALL / FORWARD ALL❌✅
裸 n❌(已废弃写法)⚠️ 可用但已废弃,建议写 FORWARD n

语法与用法 ​

FETCH 语法原型 ​

text
FETCH [ 方向 { FROM | IN } ] 游标 INTO 目标;

方向 可省略,省略时等价于 NEXT。

text
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列数与变量数须一致

最小可运行示例 ​

sql
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;
$$;
text
NOTICE:  第一行: 1 - 机械键盘
NOTICE:  第二行: 2 - 显示器
DO

逐列取值 ​

不一定要用 record,也可以直接取到标量变量:

sql
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;
$$;
text
NOTICE:  1 号商品 机械键盘 售价 599.00
DO

变量的数量与顺序必须与查询的列一一对应,否则报错。

MOVE 语法原型 ​

text
MOVE [ 方向 { FROM | IN } ] 游标;

MOVE 没有 INTO——它不返回数据。

text
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 ​

sql
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;
$$;
text
NOTICE:  跳过了 1 行
NOTICE:  当前行: 2 - 显示器
DO

CLOSE 语法原型 ​

PL/pgSQL 里关闭单个游标:

text
CLOSE 游标变量;

SQL 层可以关一个或全部:

text
CLOSE 游标名;
CLOSE ALL;
形式层说明
CLOSE 变量;PL/pgSQL按变量持有的门户名关闭
CLOSE 名字;SQL按门户名关闭
CLOSE ALL;SQL关闭当前会话所有门户

关闭未打开的游标会报错 ​

sql
-- 错误!关闭一个尚未打开的绑定游标
DO $$
DECLARE
    cur CURSOR FOR SELECT 1;
BEGIN
    CLOSE cur;      -- ERROR: cursor "cur" is not open
END;
$$;
text
ERROR:  cursor "cur" is not open

在 PG 16 之前,绑定游标变量初始值就是变量名,这个错误报的是 cursor "cur" is not open;现在语义更明确。

重复关闭同一个游标同样报错:

sql
-- 错误!重复关闭
DO $$
DECLARE
    cur refcursor;
BEGIN
    OPEN cur FOR SELECT 1;
    CLOSE cur;
    CLOSE cur;      -- ERROR: cursor "..." is not open
END;
$$;
text
ERROR:  cursor "<unnamed portal 2>" is not open

详细说明 ​

FETCH 的一个常见误解:多行形式在 PL/pgSQL 里不存在 ​

这是最值得单独强调的一点。很多从 SQL 层 FETCH 迁移过来的写法会全部失败:

sql
-- 错误!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;
$$;
text
ERROR:  syntax error at or near "ALL"
LINE 7:     FETCH ALL FROM cur INTO r;

正确写法是显式循环:

sql
-- 推荐:用循环代替 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;
$$;
text
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(或重读一次)
sql
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;
$$;
text
NOTICE:  ABSOLUTE 2 -> 显示器
NOTICE:  ABSOLUTE -1 -> 鼠标垫
NOTICE:  RELATIVE -2 -> 机械键盘
DO

负号方向(向前)与 RELATIVE 0 都要求游标可向后读,也就是必须是 SCROLL 的。

sql
-- 错误!非 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;
$$;
text
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 可以更新「当前行」:

sql
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;
$$;
text
NOTICE:  调价: 1 -> 658.9
NOTICE:  调价: 3 -> 328.9
DO

要点:

要求说明
游标查询须带 FOR UPDATE / FOR SHARE否则 WHERE CURRENT OF 报错
查询不能有分组、聚合、去重等行无法与基表行对应
若查询涉及多表连接WHERE CURRENT OF 只更新游标基表
游标不能是 SCROLLSCROLL 与 FOR UPDATE 互斥

MOVE 的多行形式与计数 ​

MOVE 的多行形式正是「跳过若干行」的利器:

sql
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;
$$;
text
NOTICE:  前进了 2 行
NOTICE:  现在在 id = 3
NOTICE:  剩余 0 行被跳过
DO

越过末尾后 MOVE 不报错,只是 ROW_COUNT 小于请求的行数——这种「软越界」让它很适合分页实现。

MOVE 到越界的返回值 ​

请求结果集剩余ROW_COUNTFOUND
MOVE FORWARD 53 行3true(移动了行)
MOVE FORWARD 50 行0false
MOVE ALL4 行4true

注意 MOVE 越界不会跳过错误,它只是「尽可能移动」。

CLOSE 的两种回收路径 ​

路径触发是否可再次 FETCH
显式 CLOSE你调用 CLOSE否,报 cursor ... is not open
隐式关闭事务结束否,门户不存在

在循环里要特别注意配对:每次 OPEN 都要有一个对应的 CLOSE。

sql
-- 推荐:每次 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;

游标已被关闭时的表现 ​

sql
-- 错误!对已关闭游标 FETCH
DO $$
DECLARE
    cur refcursor;
    r   record;
BEGIN
    OPEN cur FOR SELECT 1;
    CLOSE cur;
    FETCH cur INTO r;      -- ERROR
END;
$$;
text
ERROR:  cursor "<unnamed portal 1>" is not open

对应的 SQLSTATE 是 34000(invalid_cursor_name 类)或 24000(invalid_cursor_state),具体取决于是「名字不存在」还是「状态不对」。

部分取数:分批返回避免内存爆掉 ​

游标最大的实用价值是「分批处理」。下面这个函数每次只取 batch_size 行:

sql
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);
text
NOTICE:  已处理 1 行
NOTICE:  已处理 2 行
NOTICE:  已处理 3 行
 process_in_batches
--------------------
                  3
(1 row)

游标在 EXIT 之前的清理 ​

因为 EXCEPTION 块会回滚整块内的变更,所以「游标在异常块里没关掉」通常不会造成持久问题——门户会随子事务一起释放。但在正常的 BEGIN ... END 流程里,提前 EXIT 而忘记 CLOSE 会让门户留到事务结束。

sql
-- 不推荐:提前 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;
$$;
sql
-- 推荐:无论怎样都关闭
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;
$$;

实战示例 ​

示例一:完整遍历并输出每一行 ​

需求:打印商品列表,每行一个。

sql
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;
$$;
text
NOTICE:  1 | 机械键盘 | 599.00
NOTICE:  2 | 显示器 | 1299.00
NOTICE:  3 | 鼠标垫 | 299.00
DO

示例二:跳过前 N 行再取 ​

需求:分页取第 2 页(每页 2 行),即跳过前 2 行后取 2 行。

sql
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;
$$;
text
NOTICE:  跳过 2 行
NOTICE:  第 1 条: 鼠标垫
DO

示例三:用 WHERE CURRENT OF 批量调价 ​

需求:把所有低于 500 元的商品价格上调 10%。

sql
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;
$$;
text
NOTICE:  共调整 2 条
DO

示例四:统计到结尾的行数 ​

需求:用 MOVE ALL 快速得到剩余行数。

sql
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;
$$;
text
NOTICE:  一共 3 行
DO

示例五:SCROLL 游标前后翻页 ​

需求:模拟一个「上一页 / 下一页」的浏览行为。

sql
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;
$$;
text
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 PRIORcursor can only scan forward声明为 SCROLL
WHERE CURRENT OF 但无 FOR UPDATE报错游标查询加 FOR UPDATE
重复 CLOSE curcursor ... 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 NEXTFETCH ALL(PL/pgSQL 不支持)
跳过 N 行MOVE FORWARD N + ROW_COUNT循环 FETCH N 次丢弃
判断是否取完EXIT WHEN NOT FOUND靠 id 是否为空判断
更新当前行WHERE CURRENT OF + FOR UPDATE用主键再查一次
向前读SCROLL 游标默认游标赌运气
sql
-- 推荐:标准遍历范式
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 自动完成「打开 → 逐行取 → 关闭」,什么时候该用它替代显式游标。

参考链接 ​