隐式游标(FOR IN)
PL/pgSQL 提供了两条「不用手写游标」的循环语句:FOR 记录变量 IN 查询 LOOP 与 FOR 记录变量 IN 游标 LOOP。它们内部会自动完成「打开 → 逐行取 → 在退出时关闭」的全部动作,编译器还替你把退出条件安排好了。本节讲清这两种 FOR ... IN 循环(社区常称「隐式游标」)的语义、它们与显式游标的取舍,以及几个容易被忽略的细节:循环变量只在循环内可见、FOUND 的行为、参数化游标的循环写法。
术语澄清
严格来说,PL/pgSQL 并没有一个叫「隐式游标」的类型。所谓「隐式游标(implicit cursor)」是业界对 FOR ... IN 查询 这种由系统自动管理游标的说法的沿用。PostgreSQL 官方文档把它归入 LOOP 控制结构。本节沿用这个通俗叫法,但内容以官方语义为准。
前置知识
阅读本节前,建议先了解:FETCH / MOVE / CLOSE
基础概念
FOR ... IN 有两种数据来源,语义几乎一致,只是「结果从哪来」不同。
| 形式 | 结果来源 | 语法 |
|---|---|---|
| 查询循环 | 一条 SELECT / VALUES | FOR r IN SELECT ... LOOP |
| 游标循环 | 一个已声明的绑定游标 | FOR r IN 游标 [ ( 参数 ) ] LOOP |
两者共同的特征:你只写循环体,取行与关闭由系统负责。
显式游标 隐式游标(FOR ... IN)
────────────────────── ──────────────────────
DECLARE cur CURSOR FOR ... FOR r IN SELECT ... LOOP
OPEN cur; -- 循环体
LOOP ...
FETCH NEXT FROM cur INTO r; END LOOP;
EXIT WHEN NOT FOUND;
...
END LOOP;
CLOSE cur;记录变量由系统隐式声明
FOR 循环的循环变量不需要在 DECLARE 段声明:
-- 正确:r 在 FOR 里就地声明
DO $$
BEGIN
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
END;
$$;NOTICE: 1 - 机械键盘
NOTICE: 2 - 显示器
NOTICE: 3 - 鼠标垫
DO这个变量:
| 性质 | 说明 |
|---|---|
| 类型 | record(RECORD) |
| 作用域 | 仅限循环体,出了 END LOOP 就消失 |
是否可在 DECLARE 声明 | 不需要,也不能重复声明同名 |
| 每轮是否重新赋值 | 是,每轮指向当前行 |
退出条件自动生成
显式游标要自己写 EXIT WHEN NOT FOUND;FOR ... IN 等价于:
LOOP
FETCH 游标 INTO 记录变量;
EXIT WHEN NOT FOUND;
... 循环体 ...
END LOOP;也就是说,取不到行时自动退出,不需要你判断。这是它最省心的地方。
循环变量的可见性
循环变量是「局部于循环」的。这一点在嵌套循环里尤其重要:
DO $$
BEGIN
FOR r IN SELECT id FROM products ORDER BY id LOOP
RAISE NOTICE '外层 r.id = %', r.id;
END LOOP;
-- 这里无法访问 r,r 已超出作用域
END;
$$;如果外层已经有一个同名变量,FOR 里的声明会遮蔽(shadow)它:
DO $$
DECLARE
r text := '外层变量';
BEGIN
FOR r IN SELECT id FROM products ORDER BY id LIMIT 1 LOOP
RAISE NOTICE '循环内 r 是记录: %', r.id; -- 遮蔽了 text 的 r
END LOOP;
RAISE NOTICE '循环外 r 仍是: %', r; -- 回到 text
END;
$$;NOTICE: 循环内 r 是记录: 1
NOTICE: 循环外 r 仍是: 外层变量
DO可以用已声明的变量做循环变量吗
可以,但循环内会忽略它原有的定义。官方文档的原话是:
The variable
recordvaris automatically defined as typerecordand exists only inside the loop (any existing definition of the variable name is ignored within the loop).
也就是说,FOR 循环变量总是被自动声明为 record 类型,与你在 DECLARE 里声明的同名变量无关;出了 END LOOP 之后,原有变量恢复可用。
-- 可运行,但不推荐:预先声明的 r 在循环内被忽略
DO $$
DECLARE
r record; -- 这个声明的 r 在循环内看不到
BEGIN
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
RAISE NOTICE '循环内: % -> %', r.id, r.name;
END LOOP;
END;
$$;NOTICE: 循环内: 1 -> 机械键盘
NOTICE: 循环内: 2 -> 显示器
NOTICE: 循环内: 3 -> 鼠标垫
DO既然原有定义会被忽略,那么「预先声明一个同名变量」只会让读者困惑,属于多余代码:
-- 不推荐:DECLARE 里的 rec 纯属多余,且容易误以为能拿到循环外的值
DO $$
DECLARE
rec record;
BEGIN
FOR rec IN SELECT id, name FROM products LOOP
RAISE NOTICE '%', rec.name;
END LOOP;
-- 这里 rec 若从未被赋值,仍为 NULL
RAISE NOTICE '循环外 rec IS NULL = %', rec IS NULL;
END;
$$;-- 推荐:不预先声明,让循环变量干净地只在循环内存在
DO $$
BEGIN
FOR rec IN SELECT id, name FROM products LOOP
RAISE NOTICE '%', rec.name;
END LOOP;
END;
$$;一个真实会报错的变体
上文的「忽略」只发生在循环变量位置上。如果你声明的是一个 record 变量、却把它当成普通变量在循环体里当另一类型用,那是另一回事。真正会触发 ERROR: loop variable of loop over rows must be a record or row variable or list of scalar variables 的场景是:把已声明的标量变量(如 integer、text)直接放在循环变量位置上。
-- 错误!n 是 integer,不能作为行循环的循环变量
DO $$
DECLARE
n integer;
BEGIN
FOR n IN SELECT id FROM products LOOP
RAISE NOTICE '%', n;
END LOOP;
END;
$$;ERROR: loop variable of loop over rows must be a record or row variable or list of scalar variables
LINE 5: FOR n IN SELECT id FROM products LOOP
^
DETAIL: Number of variables specified is not equal to the number of columns returned. (或者变量的类型不受支持)要用标量承接,必须写成标量变量列表(见下一节),且列数与变量数一致。
也可以用标量变量列表
除了记录变量,FOR 还支持「多个标量变量」的形式,功能上等价于把每列拆开:
DO $$
BEGIN
FOR v_id, v_name IN SELECT id, name FROM products ORDER BY id LOOP
RAISE NOTICE '% -> %', v_id, v_name;
END LOOP;
END;
$$;NOTICE: 1 -> 机械键盘
NOTICE: 2 -> 显示器
NOTICE: 3 -> 鼠标垫
DO| 循环变量形式 | 写法 | 说明 |
|---|---|---|
| 单个记录变量 | FOR r IN ... | 通过 r.列名 访问 |
| 多个标量变量 | FOR a, b, c IN ... | 列数须与变量数一致 |
查询循环 vs 游标循环
| 对比项 | FOR r IN SELECT ... | FOR r IN 游标 LOOP |
|---|---|---|
| 查询何时确定 | 循环开始时 | 游标声明时 |
| 是否可传参 | 直接写 WHERE 条件 | 用游标形参 |
| 是否复用游标定义 | 否 | 是 |
| 是否自动开关游标 | 是 | 是 |
能否带 SCROLL | 不适用 | 不适用(循环只向前) |
一个实用结论:如果你已经有一个绑定游标,且需要反复用它遍历,就用游标循环;否则直接写查询循环。
语法与用法
语法原型
[ <<标签>> ]
FOR 目标 IN 查询 LOOP
语句;
END LOOP [ 标签 ];
[ <<标签>> ]
FOR 目标 IN 绑定游标 [ ( 参数 [, ...] ) ] LOOP
语句;
END LOOP [ 标签 ];其中 目标 是「记录变量」或「标量变量列表」。
最小可运行示例
DO $$
BEGIN
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
END;
$$;NOTICE: 1 - 机械键盘
NOTICE: 2 - 显示器
NOTICE: 3 - 鼠标垫
DO游标循环示例
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM products ORDER BY id;
BEGIN
FOR r IN cur LOOP
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
END;
$$;NOTICE: 1 - 机械键盘
NOTICE: 2 - 显示器
NOTICE: 3 - 鼠标垫
DO注意这里没有 OPEN 也没有 CLOSE——FOR 循环自己包办了。
带参游标的循环
DO $$
DECLARE
cur CURSOR (min_price numeric) FOR
SELECT name, price FROM products WHERE price >= min_price ORDER BY price;
BEGIN
FOR r IN cur (500) LOOP
RAISE NOTICE '% = %', r.name, r.price;
END LOOP;
END;
$$;NOTICE: 机械键盘 = 599.00
NOTICE: 显示器 = 1299.00
DO参数写在游标名后的括号里,与 OPEN cur (500) 的写法一致。
循环内提前退出
EXIT 与 CONTINUE 在 FOR ... IN 里照常可用:
DO $$
BEGIN
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
CONTINUE WHEN r.id = 2; -- 跳过 id=2
RAISE NOTICE '处理 %', r.name;
EXIT WHEN r.id = 3; -- 到 id=3 就停
END LOOP;
END;
$$;NOTICE: 处理 机械键盘
NOTICE: 处理 鼠标垫
DO带标签的嵌套循环
DO $$
BEGIN
<<outer_loop>>
FOR r IN SELECT id FROM products ORDER BY id LOOP
FOR s IN SELECT id FROM products ORDER BY id LOOP
EXIT outer_loop WHEN r.id + s.id > 4; -- 一次跳出两层
RAISE NOTICE 'r=% s=%', r.id, s.id;
END LOOP;
END LOOP outer_loop;
END;
$$;NOTICE: r=1 s=1
NOTICE: r=1 s=2
NOTICE: r=1 s=3
NOTICE: r=2 s=1
NOTICE: r=2 s=2
DO详细说明
查询循环背后发生了什么
FOR r IN SELECT ... LOOP 在语义上完全等价于:
DECLARE
内部游标 refcursor;
BEGIN
OPEN 内部游标 FOR SELECT ...;
LOOP
FETCH 内部游标 INTO r;
EXIT WHEN NOT FOUND;
... 循环体 ...
END LOOP;
CLOSE 内部游标; -- 正常退出与异常退出都会执行
END;这个「等价图」解释了三条行为:
| 行为 | 原因 |
|---|---|
| 循环结束游标必被关闭 | 编译器在循环出口插入关闭逻辑 |
| 循环体抛异常也会关闭 | 内部游标挂在子事务上,异常时一并释放 |
不能对循环用 FETCH PRIOR | 内部游标是向前读的,没有 SCROLL |
FOUND 在循环里的行为
FOUND 在 FOR ... IN 循环内外都有定义,但取值有讲究:
| 位置 | FOUND 的值 |
|---|---|
| 循环体内(每轮开始) | true |
| 循环正常结束后 | false |
循环体里的 SELECT INTO / UPDATE 之后 | 被那条语句覆盖 |
DO $$
DECLARE
dummy int;
BEGIN
FOR r IN SELECT id FROM products ORDER BY id LOOP
RAISE NOTICE '循环内 FOUND = %', FOUND; -- true
SELECT 1 INTO dummy; -- 覆盖了 FOUND
RAISE NOTICE 'SELECT INTO 后 FOUND = %', FOUND;
END LOOP;
RAISE NOTICE '循环后 FOUND = %', FOUND; -- false
END;
$$;NOTICE: 循环内 FOUND = t
NOTICE: SELECT INTO 后 FOUND = t
NOTICE: 循环内 FOUND = t
NOTICE: SELECT INTO 后 FOUND = t
NOTICE: 循环内 FOUND = t
NOTICE: SELECT INTO 后 FOUND = t
NOTICE: 循环后 FOUND = f
DO在 FOR 循环体内不要依赖 FOUND,因为它会被循环体内的其他语句覆盖。循环本身不需要 FOUND——退出是自动的。
循环变量是只读的吗
循环变量本质上是一个记录变量,你可以给它赋值,但这样做没有意义并且容易出错:
-- 不推荐:改写循环变量
DO $$
BEGIN
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
r.name := '被改了'; -- 允许,但下一轮会被覆盖
RAISE NOTICE '%', r.id;
END LOOP;
END;
$$;改动只在本轮有效,FOR 下一轮会重新赋值。而且这个改动不会写回表——FOR ... IN 是只读遍历,没有 WHERE CURRENT OF 那种「当前行」概念。
循环查询里的变量替换
FOR ... IN SELECT 里的查询,和绑定游标一样,会引用外层 PL/pgSQL 变量:
DO $$
DECLARE
min_price numeric := 500;
BEGIN
FOR r IN SELECT name, price FROM products WHERE price >= min_price ORDER BY price LOOP
RAISE NOTICE '% 超过阈值', r.name;
END LOOP;
END;
$$;NOTICE: 机械键盘 超过阈值
NOTICE: 显示器 超过阈值
DO但要小心歧义:如果查询里的名字同时是「表列名」和「PL/pgSQL 变量名」,默认按列名解释。
-- 陷阱:min_price 若也是列名,这里取的是列
DO $$
DECLARE
min_price numeric := 500;
BEGIN
FOR r IN SELECT name FROM products WHERE price >= min_price LOOP
RAISE NOTICE '%', r.name;
END LOOP;
END;
$$;要强制用变量,可以给变量换个不会冲突的名字,或在 PL/pgSQL 里用 function_name.变量名 / 在查询里用参数化游标。最省事的做法是给 PL/pgSQL 变量加前缀(如 v_min_price)。
设计目标:为什么官方推荐它
官方文档明确推荐:能用 FOR ... IN 就优先用它,而不是手写游标。理由是少犯三类错误:
| 手写游标易犯的错 | FOR ... IN 如何避免 |
|---|---|
忘记 OPEN | 不需要 |
忘记 CLOSE | 自动关闭 |
忘记 EXIT WHEN NOT FOUND | 自动退出 |
只有当你要显式控制读取位置(如 MOVE、FETCH PRIOR)或把游标返回给客户端时,才需要显式游标。
性能:隐式游标会不会更慢
不会。FOR ... IN 内部就是一个门户,与手写游标在服务端的执行方式一致。唯一的差别是它总是向前扫描,因此不能带 SCROLL。
| 维度 | FOR ... IN | 手写游标 |
|---|---|---|
| 服务端机制 | 门户 | 门户 |
| 是否可向后 | 否 | 可(需 SCROLL) |
| 是否可返回客户端 | 否 | 可 |
| 代码量 | 少 | 多 |
网络往返与分批
FOR ... IN 一次取一行的模型意味着:如果循环体里做了额外查询(N+1 问题),性能会急剧下降。
-- 不推荐:循环体内再查一次(N+1)
DO $$
BEGIN
FOR o IN SELECT id FROM orders LOOP
FOR d IN SELECT qty FROM order_items WHERE order_id = o.id LOOP
RAISE NOTICE '订单 % 数量 %', o.id, d.qty;
END LOOP;
END LOOP;
END;
$$;-- 推荐:用一次连接把数据取全
DO $$
BEGIN
FOR r IN
SELECT o.id, d.qty
FROM orders o
JOIN order_items d ON d.order_id = o.id
ORDER BY o.id
LOOP
RAISE NOTICE '订单 % 数量 %', r.id, r.qty;
END LOOP;
END;
$$;| 方式 | 查询次数 | 复杂度 |
|---|---|---|
| 嵌套循环 | 1 + N | 可能 O(N²) |
| 单次连接 | 1 | 一遍扫描 |
FOR ... IN 与 EXECUTE 结合
查询需要动态构造时,可以配合 EXECUTE:
DO $$
DECLARE
sql text;
r record;
BEGIN
sql := 'SELECT id, name FROM products WHERE price >= $1 ORDER BY id';
FOR r IN EXECUTE sql USING 500 LOOP
RAISE NOTICE '% -> %', r.id, r.name;
END LOOP;
END;
$$;NOTICE: 1 -> 机械键盘
NOTICE: 2 -> 显示器
DOFOR ... IN EXECUTE 是官方支持的写法,等价于「动态查询 + 隐式游标」。它比 OPEN cur FOR EXECUTE 更省事。
循环变量的字段访问
record 类型的循环变量支持 r.列名 访问,也支持 r.* 展开:
DO $$
BEGIN
FOR r IN SELECT id, name, price FROM products ORDER BY id LOOP
RAISE NOTICE '整行: %', r; -- 输出 (1,机械键盘,599.00)
RAISE NOTICE '字段: %', r.name;
END LOOP;
END;
$$;NOTICE: 整行: (1,机械键盘,599.00)
NOTICE: 字段: 机械键盘
NOTICE: 整行: (2,显示器,1299.00)
NOTICE: 字段: 显示器
NOTICE: 整行: (3,鼠标垫,299.00)
NOTICE: 字段: 鼠标垫
DO如果查询用 SELECT t.*,r 就包含整表的所有列。
遍历数组 vs 遍历查询
FOR ... IN 循环专门用于「行集」。遍历数组要用 FOREACH:
-- 遍历数组:用 FOREACH
DO $$
DECLARE
nums int[] := ARRAY[10, 20, 30];
n int;
BEGIN
FOREACH n IN ARRAY nums LOOP
RAISE NOTICE '元素: %', n;
END LOOP;
END;
$$;NOTICE: 元素: 10
NOTICE: 元素: 20
NOTICE: 元素: 30
DO| 需求 | 用哪个 |
|---|---|
| 遍历查询结果行 | FOR r IN SELECT ... |
| 遍历游标 | FOR r IN 游标 |
| 遍历数组元素 | FOREACH x IN ARRAY ... |
| 遍历整数区间 | FOR i IN 1..10 |
空结果集时的行为
如果查询返回 0 行,循环体一次也不执行:
DO $$
DECLARE
cnt int := 0;
BEGIN
FOR r IN SELECT id FROM products WHERE price > 99999 LOOP
cnt := cnt + 1;
END LOOP;
RAISE NOTICE '执行了 % 次', cnt;
END;
$$;NOTICE: 执行了 0 次
DO这和「SELECT INTO STRICT 无数据报错」不同:FOR ... IN 对空结果集是静默零次迭代。
游标循环不能带 INSENSITIVE 等选项
游标循环只能对一个已存在的绑定游标做遍历,不能内联声明 SCROLL / NO SCROLL:
-- 错误!循环里不能重新声明游标选项
DO $$
BEGIN
FOR r IN SCROLL CURSOR FOR SELECT 1 LOOP -- 语法错误
RAISE NOTICE '%', r;
END LOOP;
END;
$$;ERROR: syntax error at or near "SCROLL"要使用循环遍历游标,游标必须先在 DECLARE 里声明好。
带标签退出:多层循环的正确退出方式
嵌套 FOR ... IN 时,退出外层循环需要标签:
DO $$
BEGIN
<<outer>>
FOR a IN SELECT id FROM products ORDER BY id LOOP
FOR b IN SELECT id FROM products ORDER BY id LOOP
IF a.id = 1 AND b.id = 2 THEN
EXIT outer; -- 直接跳出两层
END IF;
RAISE NOTICE 'a=% b=%', a.id, b.id;
END LOOP;
END LOOP outer;
END;
$$;NOTICE: a=1 b=1
DO| 写法 | 退出层级 |
|---|---|
EXIT; | 最内层循环 |
EXIT 标签; | 标签标识的那层循环 |
CONTINUE; | 跳过本轮(最内层) |
CONTINUE 标签; | 跳到标签那层的下一轮 |
实战示例
示例一:遍历并汇总
需求:统计商品总价值(价格之和)。
DO $$
DECLARE
total numeric := 0;
BEGIN
FOR r IN SELECT price FROM products ORDER BY id LOOP
total := total + r.price;
END LOOP;
RAISE NOTICE '总价值 = %', total;
END;
$$;NOTICE: 总价值 = 2197.00
DO示例二:带参游标循环
需求:遍历价格不低于给定值的商品,打印名称和价格。
DO $$
DECLARE
cur CURSOR (p_min numeric) FOR
SELECT name, price FROM products WHERE price >= p_min ORDER BY price;
BEGIN
FOR r IN cur (300) LOOP
RAISE NOTICE '%: %', r.name, r.price;
END LOOP;
END;
$$;NOTICE: 机械键盘: 599.00
NOTICE: 显示器: 1299.00
DO示例三:嵌套循环 + 标签退出
需求:从订单与明细两张表里找出「同一订单有多条明细」的前若干组合,遇到条件即停。
DO $$
BEGIN
<<outer_loop>>
FOR o IN SELECT id FROM orders ORDER BY id LOOP
FOR d IN SELECT id, qty FROM order_items WHERE order_id = o.id ORDER BY id LOOP
RAISE NOTICE '订单 % 明细 % 数量 %', o.id, d.id, d.qty;
EXIT outer_loop WHEN d.qty > 10;
END LOOP;
END LOOP outer_loop;
END;
$$;NOTICE: 订单 1001 明细 1 数量 3
NOTICE: 订单 1001 明细 2 数量 15
DO示例四:动态 SQL + 隐式游标
需求:按传入的最小价格动态构造查询并遍历。
CREATE OR REPLACE FUNCTION list_expensive(p_min numeric)
RETURNS int AS $$
DECLARE
sql text := 'SELECT name, price FROM products WHERE price >= $1 ORDER BY price';
n int := 0;
r record;
BEGIN
FOR r IN EXECUTE sql USING p_min LOOP
RAISE NOTICE '% 卖 %', r.name, r.price;
n := n + 1;
END LOOP;
RETURN n;
END;
$$ LANGUAGE plpgsql;
SELECT list_expensive(500);NOTICE: 机械键盘 卖 599.00
NOTICE: 显示器 卖 1299.00
list_expensive
----------------
2
(1 row)示例五:用循环建汇总表
需求:按固定分档统计每个价格档的商品数量。
CREATE TABLE IF NOT EXISTS price_buckets (
label text PRIMARY KEY,
cnt int
);
DO $$
DECLARE
bucket record;
BEGIN
FOR bucket IN
SELECT '低价' AS label, count(*) AS cnt FROM products WHERE price < 500
UNION ALL
SELECT '中价', count(*) FROM products WHERE price >= 500 AND price < 1000
UNION ALL
SELECT '高价', count(*) FROM products WHERE price >= 1000
LOOP
INSERT INTO price_buckets VALUES (bucket.label, bucket.cnt)
ON CONFLICT (label) DO UPDATE SET cnt = EXCLUDED.cnt;
RAISE NOTICE '%: %', bucket.label, bucket.cnt;
END LOOP;
END;
$$;NOTICE: 低价: 1
NOTICE: 中价: 1
NOTICE: 高价: 1
DO注意事项
- 循环变量无需声明,也不能与
DECLARE段已有变量同名。 - 循环变量只在循环体内可见;出了
END LOOP失效。 - 循环变量赋值不影响表数据,
FOR ... IN是只读遍历。 - 退出是自动的,不要在循环里写
EXIT WHEN NOT FOUND(FOUND会被循环体语句覆盖)。 FOR ... IN只向前读,不支持SCROLL。- 嵌套循环退出外层要用标签。
- 循环内不要在
FOR上叠 N+1 查询,优先用连接。 - 变量与列名同名时按列名解释,给变量加前缀规避。
| 错误写法 | 原因 | 正确做法 |
|---|---|---|
DECLARE r record; 后又 FOR r IN ... | 变量重复 | 循环变量不预先声明,或改用别的名字 |
循环里写 EXIT WHEN NOT FOUND | FOUND 已被覆盖,逻辑错乱 | 依赖自动退出 |
FOR r IN cur,其中 cur 未声明为游标 | 编译错误 | 先在 DECLARE 声明绑定游标 |
FOR r IN SCROLL CURSOR FOR ... | 语法错误 | 游标须事先声明 |
| 期望循环变量的修改写回表 | FOR ... IN 只读 | 用游标 + WHERE CURRENT OF 或 UPDATE |
最佳实践
- 默认用
FOR r IN SELECT ... LOOP,只在需要复用游标定义或控制位置时才用显式游标。 - 给 PL/pgSQL 变量加前缀(
v_)以避免与列名混淆。 - 避免循环内再查(N+1):用
JOIN/ 聚合一次取全。 - 多层循环用标签,让
EXIT/CONTINUE意图明确。 - 需要向后读、需要返回游标给客户端时,改用显式游标。
- 遍历数组用
FOREACH、遍历区间用FOR i IN 1..n,不要硬套查询循环。
| 场景 | 推荐 | 反例 |
|---|---|---|
| 遍历查询结果 | FOR r IN SELECT ... | 手写 OPEN/FETCH/CLOSE |
| 复用游标定义 | FOR r IN 游标 (参数) | 每次重写 SELECT |
| 需要向后读 | 显式 SCROLL 游标 | 用 FOR 循环(做不到) |
| 返回结果给客户端 | RETURNS refcursor | 用 FOR 循环收集再返回 |
| 遍历数组 | FOREACH | FOR r IN SELECT unnest(...) |
-- 推荐:简洁、自动管理
FOR r IN SELECT id, name FROM products ORDER BY id LOOP
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
-- 不推荐:手写一切
OPEN cur;
LOOP
FETCH cur INTO r;
EXIT WHEN NOT FOUND;
RAISE NOTICE '% - %', r.id, r.name;
END LOOP;
CLOSE cur;下一节
下一节我们学习:异常处理(EXCEPTION),理解 PL/pgSQL 如何用 BEGIN ... EXCEPTION ... END 捕获错误、如何用子事务保护部分逻辑,以及异常块对已执行变更的回滚边界。