遍历查询结果(FOR ... IN SELECT)
上一节讲的整数 FOR 只能遍历数字区间。真实业务里更常见的是「遍历一张表里满足条件的每一行」——这就需要 FOR ... IN SELECT。它把查询结果逐行喂给循环体,是 PL/pgSQL 中最常用的循环形态,几乎每个批处理函数里都会出现。
本节讲解它的语法、目标变量的三种形态、循环如何在内部借助游标逐行获取以避免内存问题,以及如何与 EXECUTE 组合成动态查询循环。
前置知识
阅读本节前,建议先了解:循环(LOOP / WHILE / FOR)
基础概念
它解决什么问题
整数 FOR 的语法是 FOR i IN 1..10 LOOP,区间两端必须是整数。而业务需要的是:
- 遍历
SELECT * FROM orders WHERE status = 'new'的每一行; - 对每一行做判断、更新或调用其他函数;
- 行的结构往往和表结构一致。
FOR ... IN SELECT 就是为这件事设计的:它把「查询」当作循环的驱动源,每取一行就执行一次循环体。
FOR 目标变量 IN 查询 LOOP
│ │
│ └── 查询在内部作为游标逐行获取
└── 每轮取一行赋给目标变量与整数 FOR 的对比
| 对比项 | FOR i IN 1..10 | FOR r IN SELECT ... |
|---|---|---|
| 循环变量来源 | 自动声明为 integer | 由查询结果决定(record / %ROWTYPE / 标量列表) |
| 推进方式 | 自动加步长 | 逐行取结果集 |
| 循环次数 | 由区间决定 | 由结果集行数决定 |
| 提前退出 | EXIT / CONTINUE | 同左 |
| 结果集为空时 | 区间为空则零次 | 零行则零次 |
三种目标变量形态
| 形态 | 写法 | 适用场景 |
|---|---|---|
行变量(RECORD 或 %ROWTYPE) | FOR r IN SELECT * FROM t LOOP | 需要整行所有列 |
| 逗号分隔的标量列表 | FOR a, b IN SELECT x, y FROM t LOOP | 只需要少数几列 |
| 记录类型的组合 | FOR a, b IN SELECT x, y FROM t LOOP | 列的个数与变量个数必须一致 |
如果用 RECORD 作为目标变量,它的结构由每次查询的行结构决定;如果用 %ROWTYPE,则必须在 DECLARE 段先声明类型。
三个关键事实
- 内部自动使用游标逐行获取。官方文档明确说明:PL/pgSQL 用户通常不需要担心游标相关的内存问题,因为
FOR循环会自动在内部使用游标以避免内存问题。也就是说,循环不会把整个结果集一次性读进内存。 - 循环内部的查询计划会被缓存。
FOR查询里的 PL/pgSQL 变量会被替换成查询参数,并且查询计划会被缓存以便复用。 FOR ... IN EXECUTE支持动态查询,语法是FOR ... IN EXECUTE 字符串 [USING 参数];与普通EXECUTE一样,它每次进入循环都会重新规划,不走计划缓存。
语法与用法
完整语法:
[ <<label>> ]
FOR 目标 [ , ... ] IN 查询 LOOP
语句;
END LOOP [ label ];
[ <<label>> ]
FOR 目标 [ , ... ] IN EXECUTE 字符串 [ USING 表达式 [ , ... ] ] LOOP
语句;
END LOOP [ label ];语法要点:
FOR 目标 IN 查询 LOOP
│ │
│ ├── 普通查询:可以是任意 SELECT(含 JOIN、CTE)
│ ├── 查询在内部以游标方式逐行获取(不整体读入内存)
│ └── 查询的行数决定循环圈数
│
├── 单个目标:必须是 record 或 %ROWTYPE 类型的变量
└── 多个目标:必须是逗号分隔的标量变量,个数与列数一致
FOR 目标 IN EXECUTE 字符串 [USING ...] LOOP
│ │
│ ├── 字符串在运行时拼装(动态 SQL)
│ └── USING 里的表达式绑定为 $1、$2 ...
│
└── 目标同样可以是 record / %ROWTYPE / 标量列表最小示例:
-- 形态一:行变量
DO $$
DECLARE
v_r record;
BEGIN
FOR v_r IN
SELECT relname, relkind FROM pg_class WHERE relkind = 'r' ORDER BY relname LIMIT 3
LOOP
RAISE NOTICE '表 % 类型 %', v_r.relname, v_r.relkind;
END LOOP;
END;
$$;
-- 形态二:标量列表
DO $$
DECLARE
v_name text;
v_kind "char";
BEGIN
FOR v_name, v_kind IN
SELECT relname, relkind FROM pg_class WHERE relkind = 'i' ORDER BY relname LIMIT 2
LOOP
RAISE NOTICE '索引 % 类型 %', v_name, v_kind;
END LOOP;
END;
$$;详细说明
行变量形态:最常用也最省心
当需要整行数据时,用 record 或 %ROWTYPE 变量接收。record 不需要提前声明字段,结构随行变化;%ROWTYPE 与指定表绑定,有编译期类型检查。
CREATE OR REPLACE FUNCTION app_scan_staff(p_min_salary numeric)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_r record;
v_cnt int := 0;
BEGIN
FOR v_r IN
SELECT id, name, dept, salary
FROM demo_staff
WHERE salary >= p_min_salary
ORDER BY salary DESC
LOOP
RAISE NOTICE '员工 % (% ) 薪资 %', v_r.name, v_r.dept, v_r.salary;
v_cnt := v_cnt + 1;
END LOOP;
RETURN v_cnt;
END;
$$;用 %ROWTYPE 写法语义更紧,且能提前发现列名拼写错误:
CREATE OR REPLACE FUNCTION app_scan_staff_typed()
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_r demo_staff%ROWTYPE;
v_cnt int := 0;
BEGIN
FOR v_r IN SELECT * FROM demo_staff WHERE active LOOP
RAISE NOTICE '% 的薪资是 %', v_r.name, v_r.salary;
v_cnt := v_cnt + 1;
END LOOP;
RETURN v_cnt;
END;
$$;注意:用 %ROWTYPE 接收时,SELECT 的列必须与该表完全一致(SELECT * 最典型)。如果只选了几列,会导致「结构不匹配」的运行时报错。
-- 错误!demo_staff%ROWTYPE 有四列,这里只选了两列
-- FOR v_r IN SELECT id, name FROM demo_staff LOOP ... END LOOP;
-- 报错:record "v_r" has no field "salary" (或列数不匹配相关错误)标量列表形态:只要几列时更清晰
不需要整行时,直接在 FOR 里列出多个变量。此时每个变量都是普通的 PL/pgSQL 变量,循环内可以随意读写(不像整数 FOR 的循环变量是只读副本)。
CREATE OR REPLACE FUNCTION app_print_pairs()
RETURNS void LANGUAGE plpgsql AS $$
DECLARE
v_dept text;
v_avg numeric;
BEGIN
FOR v_dept, v_avg IN
SELECT dept, round(avg(salary), 2)
FROM demo_staff
GROUP BY dept
ORDER BY dept
LOOP
RAISE NOTICE '部门 % 平均薪资 %', v_dept, v_avg;
v_dept := upper(v_dept); -- 合法:这是普通变量
END LOOP;
END;
$$;列数与变量数必须严格相等,否则会报错:
-- 错误!两个变量对应三列
-- FOR v_a, v_b IN SELECT id, name, salary FROM demo_staff LOOP结果集获取方式:内部游标与内存
这是 FOR ... IN SELECT 最容易误解的一点。很多资料会说它「先把整个结果集算出来再循环」,这与官方文档不符。官方文档在游标章节明确指出:
PL/pgSQL users do not normally need to worry about that, since FOR loops automatically use a cursor internally to avoid memory problems.
即:FOR 循环会自动在内部使用游标,逐行获取结果,从而避免内存问题。所以「结果集太大必须改用游标」这个说法是不准确的——FOR 循环本身已经在用游标了。
不过仍然有几点需要注意:
- 逐行获取不等于「不占内存」。循环体里如果把每行都累积进数组或临时表,内存仍会增长;占内存的是你的累积逻辑,不是
FOR本身。 - 整个循环运行在一个事务里。循环再长也不会中途提交,长事务带来的锁与回滚段压力仍然存在。
- 查询计划会被缓存。循环内的 PL/pgSQL 变量被当作查询参数,计划缓存可复用,这对重复调用很有利。
-- 这样写不会把整表读进内存,但会让循环跑很久
FOR v_r IN SELECT id, content FROM demo_msg WHERE pushed = false ORDER BY id LOOP
...
END LOOP;
-- 推荐:即便如此,批处理仍应加上限,避免单个长事务无限期运行
FOR v_r IN
SELECT id, content FROM demo_msg WHERE pushed = false ORDER BY id LIMIT 1000
LOOP
...
END LOOP;真正需要显式游标的场景是「要在遍历中途 COMMIT」,而不是「结果集大」:
-- 需要流式处理 + 中途提交时用显式游标(后续章节详述)
-- DECLARE c CURSOR FOR SELECT * FROM demo_msg WHERE pushed = false;
-- OPEN c; LOOP FETCH c INTO v_r; EXIT WHEN NOT FOUND; ... END LOOP; CLOSE c;提前退出:EXIT 与 CONTINUE
FOR ... IN SELECT 里的 EXIT / CONTINUE 与整数 FOR 完全一致,标签机制也一样。多列组合筛选时,配合标签跳过一大类记录非常方便。
CREATE OR REPLACE FUNCTION app_first_big_spender(p_threshold numeric)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
v_r record;
BEGIN
FOR v_r IN SELECT id, name, salary FROM demo_staff ORDER BY salary DESC LOOP
-- 跳过非技术部门
CONTINUE WHEN v_r.name LIKE 'test%';
IF v_r.salary >= p_threshold THEN
RETURN v_r.name; -- 找到第一个就返回
END IF;
END LOOP;
RETURN NULL; -- 没有找到
END;
$$;RETURN 会立即结束整个函数,比 EXIT 更彻底——EXIT 只是跳出循环,函数还会继续往下执行。
FOUND 在循环中的行为
官方文档对 FOUND 与 FOR 的关系有明确定义:FOR 或 FOREACH 语句如果迭代了一次或多次,就把 FOUND 置为 true,否则置为 false;这个赋值发生在循环退出时。在循环执行期间,循环语句本身不会修改 FOUND(但循环体里的其他语句可能会改它)。
这条规则意味着:
- 循环结束后
FOUND可以告诉你「本次循环有没有至少迭代过一次」,这正是判断「查询是否返回了行」的手段; - 但循环体里若执行了
UPDATE、SELECT INTO、PERFORM等语句,它们会覆盖FOUND,所以循环体内部不要依赖FOUND判断别的循环状态; - 若循环因
EXIT提前退出,最后一次迭代说明确实取到过行,FOUND仍为true。
CREATE OR REPLACE FUNCTION app_has_any(p_dept text)
RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE
v_r record;
BEGIN
FOR v_r IN SELECT * FROM demo_staff WHERE dept = p_dept LOOP
-- 循环体内不要读 FOUND:它可能被这里的其他语句改写
EXIT; -- 只要知道有没有,取到一行就够
END LOOP;
-- 官方语义:迭代过至少一次 => FOUND 为 true
RETURN FOUND;
END;
$$;如果需要「循环体内部」的计数或标志,仍应自己维护变量,因为循环体里的 DML 会不断改写 FOUND:
DECLARE
v_seen int := 0;
BEGIN
FOR v_r IN SELECT * FROM demo_staff WHERE dept = p_dept LOOP
v_seen := v_seen + 1; -- 用独立变量记录,不受 FOUND 影响
END LOOP;
RETURN v_seen;
END;FOR ... IN EXECUTE:动态查询循环
当查询文本本身需要在运行期拼装时(典型是动态表名),把 SELECT 换成 EXECUTE 字符串:
CREATE OR REPLACE FUNCTION app_scan_partition(p_ym text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE
v_r record;
v_sum numeric := 0;
BEGIN
-- 表名走 %I,状态值走 USING
FOR v_r IN EXECUTE format(
'SELECT id, amount FROM %I WHERE status = $1 ORDER BY id',
'orders_' || p_ym
)
USING 'done'
LOOP
v_sum := v_sum + v_r.amount;
END LOOP;
RETURN v_sum;
END;
$$;要点与普通的 EXECUTE 一致:
- 表名、列名用
format('%I', ...)包裹; - 值用
USING绑定,绝不拼字符串; - 与普通
FOR查询不同,IN EXECUTE的查询字符串在每次进入FOR循环时都会被重新求值并重新规划(正是这一点让它能使用动态表名,代价是没有计划缓存)。
循环中修改数据:要注意快照
FOR 循环里的查询在开始取数时就确定了它使用的快照(snapshot),此后循环体里对源表做 UPDATE,不会改变本次循环还要遍历的行集合。
CREATE OR REPLACE FUNCTION app_mark_and_count()
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_r record;
v_n int := 0;
BEGIN
-- 快照中包含所有 pushed = false 的行
FOR v_r IN SELECT id FROM demo_msg WHERE pushed = false ORDER BY id LIMIT 100 LOOP
UPDATE demo_msg SET pushed = true WHERE id = v_r.id;
v_n := v_n + 1;
END LOOP;
RETURN v_n; -- 永远是 100(或不足 100 时的实际行数)
END;
$$;这个特性通常是好事:它保证循环遍历的是一个稳定集合,不会因为自己的更新而漏行或多行。但也意味着它不适合做「边改边取」的流式消费——那种场景要用游标加 FOR UPDATE。
查询里使用 CTE 与窗口函数
FOR ... IN SELECT 的查询部分可以是任意 SELECT,包括 CTE、窗口函数、LATERAL、聚合等。这让它比游标灵活得多。
CREATE OR REPLACE FUNCTION app_top_per_dept(p_n int)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_r record;
v_cnt int := 0;
BEGIN
FOR v_r IN
WITH ranked AS (
SELECT id, name, dept, salary,
row_number() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM demo_staff
)
SELECT id, name, dept, salary
FROM ranked
WHERE rn <= p_n
ORDER BY dept, rn
LOOP
RAISE NOTICE '部门 % 第 % 名:%', v_r.dept, v_r.name;
v_cnt := v_cnt + 1;
END LOOP;
RETURN v_cnt;
END;
$$;与游标的取舍
初学者常问:什么时候用 FOR ... IN SELECT,什么时候用游标?简单的判断表:
| 维度 | FOR ... IN SELECT | 显式游标 |
|---|---|---|
| 结果集获取 | 内部自动用游标逐行取,无需手写 | 手工 OPEN/FETCH/CLOSE |
| 是否需要大结果集专用 | 不需要,本身就逐行取 | 同样逐行取 |
遍历中途能否 COMMIT | 不能,整个循环在一个事务里 | 可以(过程中的游标 + COMMIT) |
是否需要 FOR UPDATE 锁定 | 查询里可以写 | 可以在 DECLARE CURSOR 里写 |
| 使用复杂度 | 低,一行搞定 | 高,需 OPEN/FETCH/CLOSE |
| 是否支持参数化查询文本 | 支持(IN EXECUTE ... USING) | 支持 |
结论:绝大多数场景用 FOR ... IN SELECT;只有「需要在遍历中途提交事务」时才必须改用显式游标。
-- 90% 的场景这样写就够了
FOR v_r IN SELECT * FROM demo_task WHERE status = 'new' LOOP
PERFORM app_handle(v_r.id);
END LOOP;实战示例
场景一:批量重算订单金额并回写
需求:某次促销规则调整后,需要遍历所有「待支付」订单,重算应付金额并写回,同时记录处理了几单、总额是多少。
建表与造数:
CREATE TABLE IF NOT EXISTS demo_order(
id bigserial PRIMARY KEY,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
discount numeric(12,2) NOT NULL DEFAULT 0,
payable numeric(12,2),
status text NOT NULL DEFAULT 'pending'
);
INSERT INTO demo_order(user_id, amount, discount, status) VALUES
(1, 200.00, 20.00, 'pending'),
(2, 99.90, 0.00, 'pending'),
(3, 1580.00, 158.00, 'pending'),
(4, 300.00, 30.00, 'paid');建函数:
CREATE OR REPLACE FUNCTION app_recalc_payable(p_status text)
RETURNS TABLE(processed int, total_payable numeric)
LANGUAGE plpgsql AS $$
DECLARE
v_r record;
v_cnt int := 0;
v_total numeric := 0;
v_pay numeric;
BEGIN
FOR v_r IN
SELECT id, amount, discount
FROM demo_order
WHERE status = p_status
ORDER BY id
LOOP
-- 应付金额不能为负
v_pay := greatest(v_r.amount - v_r.discount, 0);
UPDATE demo_order SET payable = v_pay WHERE id = v_r.id;
v_cnt := v_cnt + 1;
v_total := v_total + v_pay;
RAISE NOTICE '订单 % 应付 %', v_r.id, v_pay;
END LOOP;
processed := v_cnt;
total_payable := v_total;
RETURN NEXT;
END;
$$;调用与输出:
SELECT * FROM app_recalc_payable('pending');NOTICE: 订单 1 应付 180.00
NOTICE: 订单 2 应付 99.90
NOTICE: 订单 3 应付 1422.00
processed | total_payable
-----------+---------------
3 | 1701.90
(1 row)验证写回结果:
SELECT id, amount, discount, payable FROM demo_order WHERE status = 'pending' ORDER BY id; id | amount | discount | payable
----+---------+----------+---------
1 | 200.00 | 20.00 | 180.00
2 | 99.90 | 0.00 | 99.90
3 | 1580.00 | 158.00 | 1422.00
(3 rows)场景二:动态遍历多个分区表做归档
需求:订单按月分表,需要一个维护函数遍历指定月份列表,把每个分区表里超过保留期的已完成订单迁到归档表,并返回各分区的迁移行数。
建表与造数:
CREATE TABLE IF NOT EXISTS orders_202601(
id bigserial PRIMARY KEY, user_id bigint, amount numeric(12,2),
status text DEFAULT 'done', created_at timestamptz DEFAULT now()
);
CREATE TABLE IF NOT EXISTS orders_202602(
id bigserial PRIMARY KEY, user_id bigint, amount numeric(12,2),
status text DEFAULT 'done', created_at timestamptz DEFAULT now()
);
CREATE TABLE IF NOT EXISTS orders_archive(
id bigint, ym text, user_id bigint, amount numeric(12,2),
status text, created_at timestamptz
);
INSERT INTO orders_202601(user_id, amount, created_at) VALUES
(1, 100.00, '2026-01-05 10:00:00+08'),
(2, 200.00, '2026-01-20 10:00:00+08');
INSERT INTO orders_202602(user_id, amount, created_at) VALUES
(3, 300.00, '2026-02-10 10:00:00+08');建函数:
CREATE OR REPLACE FUNCTION app_archive_orders(p_months text[], p_before timestamptz)
RETURNS TABLE(ym text, moved bigint)
LANGUAGE plpgsql AS $$
DECLARE
v_ym text;
v_r record;
v_moved bigint;
BEGIN
FOREACH v_ym IN ARRAY p_months LOOP
-- 参数校验:只允许 6 位数字,避免被拼进奇怪的标识符
IF v_ym !~ '^[0-9]{6}$' THEN
RAISE EXCEPTION '非法月份参数:%', v_ym USING ERRCODE = '22023';
END IF;
v_moved := 0;
-- 动态查询 + 逐行迁移
FOR v_r IN EXECUTE format(
'SELECT id, user_id, amount, status, created_at
FROM %I
WHERE status = $1 AND created_at < $2',
'orders_' || v_ym
)
USING 'done', p_before
LOOP
INSERT INTO orders_archive(id, ym, user_id, amount, status, created_at)
VALUES (v_r.id, v_ym, v_r.user_id, v_r.amount, v_r.status, v_r.created_at);
EXECUTE format('DELETE FROM %I WHERE id = $1', 'orders_' || v_ym)
USING v_r.id;
v_moved := v_moved + 1;
END LOOP;
ym := v_ym;
moved := v_moved;
RETURN NEXT;
END LOOP;
END;
$$;调用与输出:
SELECT * FROM app_archive_orders(ARRAY['202601', '202602'], '2026-02-01 00:00:00+08'); ym | moved
---------+-------
202601 | 2
202602 | 0
(2 rows)检查归档表:
SELECT id, ym, amount FROM orders_archive ORDER BY id; id | ym | amount
----+--------+--------
1 | 202601 | 100.00
2 | 202601 | 200.00
(2 rows)注意这里外层用了 FOREACH 遍历月份数组,内层用 FOR ... IN EXECUTE 遍历动态查询结果——两种循环嵌套是批处理函数的常见骨架。
注意事项
1. 仍然要给大循环加过滤与上限
虽然 FOR 循环内部用游标逐行取、不会把结果集整体读进内存,但整个循环运行在一个事务里。不加过滤地扫描全表会让这个事务长时间运行,带来锁等待与回滚段膨胀。
-- 不推荐:全表扫描,事务时间不可控
-- FOR v_r IN SELECT * FROM demo_msg LOOP ... END LOOP;
-- 推荐
FOR v_r IN SELECT id FROM demo_msg WHERE pushed = false ORDER BY id LIMIT 1000 LOOP
...
END LOOP;2. %ROWTYPE 目标要求列完全匹配
用 %ROWTYPE 接收时,查询列必须与表结构一致,否则运行时报错。
-- 错误!只选了两列
-- FOR v_r IN SELECT id, name FROM demo_staff LOOP
-- 正确
FOR v_r IN SELECT * FROM demo_staff LOOP3. 标量列表的变量个数必须等于列数
多一个少一个都会报错。
-- 错误!两变量对三列
-- FOR v_a, v_b IN SELECT id, name, salary FROM demo_staff LOOP
-- 正确
FOR v_a, v_b, v_c IN SELECT id, name, salary FROM demo_staff LOOP4. 循环体内部不要依赖 FOUND
FOR/FOREACH 只在退出循环时根据「是否迭代过至少一次」设置 FOUND;循环体里的 UPDATE、PERFORM、SELECT INTO 都会改写它。
-- 错误!循环体里刚做完 UPDATE,FOUND 已被改写
-- FOR r IN SELECT * FROM t LOOP
-- UPDATE t SET f = 1 WHERE id = r.id;
-- IF FOUND THEN ... END IF; -- 这里判断的是 UPDATE 是否影响了行,不是循环
-- END LOOP;
-- 正确:循环体内部用独立变量计数
v_seen := v_seen + 1;5. 循环里的修改不影响本次遍历集合
快照语义,这是特性不是 bug,但要清楚它意味着「不能边消费边追加」。
FOR r IN SELECT id FROM demo_msg WHERE pushed = false LOOP
UPDATE demo_msg SET pushed = true WHERE id = r.id; -- 本次循环仍按原快照推进
END LOOP;6. 不要用 FOR ... IN SELECT 做超大批量处理
行数到几十万、上百万时,单事务时长、锁持有时间与回滚段占用都会成为问题。此时应分批。
-- 不推荐:一次处理全部,事务时间不可控
-- FOR r IN SELECT * FROM demo_msg LOOP ... END LOOP;
-- 推荐:按批处理,每批完成后由外层控制提交
FOR r IN SELECT * FROM demo_msg WHERE pushed = false ORDER BY id LIMIT 5000 LOOP7. 动态查询里表名必须用 %I 包裹
否则含大写或特殊字符的表名会出错,更有注入风险。
FOR r IN EXECUTE format('SELECT * FROM %I WHERE status = $1', v_table) USING p_status LOOP8. 内层 EXIT 只跳一层
嵌套循环里想跳出外层必须打标签。
<<outer>>
FOR r IN SELECT * FROM demo_a LOOP
FOR s IN SELECT * FROM demo_b LOOP
EXIT outer; -- 明确跳出外层
END LOOP;
END LOOP outer;9. IN EXECUTE 的 USING 不能省
省掉就会退化成字符串拼接,注入风险立刻回来。
-- 错误!值被直接拼进字符串
-- FOR r IN EXECUTE 'SELECT * FROM t WHERE dept = ''' || p_dept || '''' LOOP
-- 正确
FOR r IN EXECUTE 'SELECT * FROM demo_staff WHERE dept = $1' USING p_dept LOOP10. 循环变量是完整的一行,字段访问需要正确拼写
record 变量在编译期不检查字段名,拼错要到运行期才报错。
-- 运行时才报错:record "v_r" has no field "nmae"
-- RAISE NOTICE '%', v_r.nmae;最佳实践
1. 能用集合式 SQL 就别用循环
这是最重要的原则。循环是「最后手段」,不是默认选择。
-- 推荐:一条语句
UPDATE demo_order SET payable = greatest(amount - discount, 0) WHERE status = 'pending';
-- 不推荐:循环逐行
-- FOR r IN SELECT * FROM demo_order WHERE status = 'pending' LOOP
-- UPDATE demo_order SET payable = ... WHERE id = r.id;
-- END LOOP;2. 遍历前先加 ORDER BY
结果顺序不确定时,排查问题会非常痛苦。显式排序让行为可复现。
FOR r IN SELECT id, name FROM demo_staff ORDER BY id LOOP3. 只选需要的列
减少每行传输与反序列化的开销,也避免 %ROWTYPE 的列匹配问题。
-- 推荐
FOR r IN SELECT id, name FROM demo_staff WHERE active ORDER BY id LOOP
-- 不推荐
-- FOR r IN SELECT * FROM demo_staff LOOP4. 用 %ROWTYPE 换取编译期检查
整行处理时,%ROWTYPE 比 record 更安全,列名拼错会在计划阶段暴露。
DECLARE v_r demo_staff%ROWTYPE;
BEGIN
FOR v_r IN SELECT * FROM demo_staff LOOP5. 大批量处理一律分批 + 可中断
给每批一个上限,配合进度日志,运维时可随时取消。
FOR r IN SELECT id FROM demo_msg WHERE pushed = false ORDER BY id LIMIT p_batch LOOP
...
END LOOP;6. 用 CONTINUE WHEN 做前置过滤
把不处理的记录尽早跳过,比在循环体深处写嵌套 IF 更清晰。
FOR r IN SELECT * FROM demo_staff ORDER BY id LOOP
CONTINUE WHEN NOT r.active;
CONTINUE WHEN r.salary IS NULL;
...
END LOOP;7. 循环内避免重复查询同一行
能一次查出来的字段就别在循环里再查一次。
-- 推荐:查询时一并取出
FOR r IN SELECT id, name, dept FROM demo_staff LOOP
-- r.dept 已经取到,无需再查8. 动态循环里的标识符统一走 format('%I')
形成肌肉记忆,避免漏掉任何一处。
FOR r IN EXECUTE format('SELECT * FROM %I WHERE id > $1', v_table) USING p_min_id LOOP9. 给长循环加进度日志
处理上万行时,RAISE NOTICE 每千条一次,能清楚知道卡在哪。
v_n := v_n + 1;
IF v_n % 1000 = 0 THEN
RAISE NOTICE '已处理 % 行', v_n;
END IF;10. 需要中途提交时改用游标
FOR ... IN SELECT 的整个循环在一个事务里,无法中途 COMMIT。确实需要分批提交时用显式游标。
-- 需要在遍历中途 COMMIT 时应改用显式游标 + FETCH,而非 FOR ... IN SELECT11. 把「有副作用」的操作封装成函数
循环体里只留一行函数调用,逻辑集中、便于测试与复用。
FOR r IN SELECT id, amount FROM demo_order WHERE status = 'pending' ORDER BY id LOOP
PERFORM app_settle_order(r.id, r.amount);
END LOOP;12. 用 EXCEPTION 块让单行失败不影响整批
需要「跳过坏数据、继续处理」时,把循环体包进子块。
FOR r IN SELECT id, payload FROM demo_job ORDER BY id LOOP
BEGIN
PERFORM app_run_job(r.id, r.payload);
EXCEPTION
WHEN others THEN
RAISE WARNING '作业 % 失败:%', r.id, SQLERRM;
END;
END LOOP;下一节
继续学习:遍历数组(FOREACH)