Skip to content

遍历查询结果(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 就是为这件事设计的:它把「查询」当作循环的驱动源,每取一行就执行一次循环体。

text
FOR 目标变量 IN 查询 LOOP
        │           │
        │           └── 查询在内部作为游标逐行获取
        └── 每轮取一行赋给目标变量

与整数 FOR 的对比 ​

对比项FOR i IN 1..10FOR 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 一样,它每次进入循环都会重新规划,不走计划缓存。

语法与用法 ​

完整语法:

text
[ <<label>> ]
FOR 目标 [ , ... ] IN 查询 LOOP
    语句;
END LOOP [ label ];

[ <<label>> ]
FOR 目标 [ , ... ] IN EXECUTE 字符串 [ USING 表达式 [ , ... ] ] LOOP
    语句;
END LOOP [ label ];

语法要点:

text
FOR 目标 IN 查询 LOOP
     │        │
     │        ├── 普通查询:可以是任意 SELECT(含 JOIN、CTE)
     │        ├── 查询在内部以游标方式逐行获取(不整体读入内存)
     │        └── 查询的行数决定循环圈数
     │
     ├── 单个目标:必须是 record 或 %ROWTYPE 类型的变量
     └── 多个目标:必须是逗号分隔的标量变量,个数与列数一致

FOR 目标 IN EXECUTE 字符串 [USING ...] LOOP
     │               │
     │               ├── 字符串在运行时拼装(动态 SQL)
     │               └── USING 里的表达式绑定为 $1、$2 ...
     │
     └── 目标同样可以是 record / %ROWTYPE / 标量列表

最小示例:

sql
-- 形态一:行变量
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 与指定表绑定,有编译期类型检查。

sql
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 写法语义更紧,且能提前发现列名拼写错误:

sql
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 * 最典型)。如果只选了几列,会导致「结构不匹配」的运行时报错。

sql
-- 错误!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 的循环变量是只读副本)。

sql
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;
$$;

列数与变量数必须严格相等,否则会报错:

sql
-- 错误!两个变量对应三列
-- 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 变量被当作查询参数,计划缓存可复用,这对重复调用很有利。
sql
-- 这样写不会把整表读进内存,但会让循环跑很久
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」,而不是「结果集大」:

sql
-- 需要流式处理 + 中途提交时用显式游标(后续章节详述)
-- 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 完全一致,标签机制也一样。多列组合筛选时,配合标签跳过一大类记录非常方便。

sql
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。
sql
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:

sql
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 字符串:

sql
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,不会改变本次循环还要遍历的行集合。

sql
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、聚合等。这让它比游标灵活得多。

sql
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;只有「需要在遍历中途提交事务」时才必须改用显式游标。

sql
-- 90% 的场景这样写就够了
FOR v_r IN SELECT * FROM demo_task WHERE status = 'new' LOOP
  PERFORM app_handle(v_r.id);
END LOOP;

实战示例 ​

场景一:批量重算订单金额并回写 ​

需求:某次促销规则调整后,需要遍历所有「待支付」订单,重算应付金额并写回,同时记录处理了几单、总额是多少。

建表与造数:

sql
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');

建函数:

sql
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;
$$;

调用与输出:

sql
SELECT * FROM app_recalc_payable('pending');
text
NOTICE:  订单 1 应付 180.00
NOTICE:  订单 2 应付 99.90
NOTICE:  订单 3 应付 1422.00
 processed | total_payable
-----------+---------------
         3 |       1701.90
(1 row)

验证写回结果:

sql
SELECT id, amount, discount, payable FROM demo_order WHERE status = 'pending' ORDER BY id;
text
 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)

场景二:动态遍历多个分区表做归档 ​

需求:订单按月分表,需要一个维护函数遍历指定月份列表,把每个分区表里超过保留期的已完成订单迁到归档表,并返回各分区的迁移行数。

建表与造数:

sql
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');

建函数:

sql
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;
$$;

调用与输出:

sql
SELECT * FROM app_archive_orders(ARRAY['202601', '202602'], '2026-02-01 00:00:00+08');
text
   ym    | moved
---------+-------
 202601  |     2
 202602  |     0
(2 rows)

检查归档表:

sql
SELECT id, ym, amount FROM orders_archive ORDER BY id;
text
 id |   ym   | amount
----+--------+--------
  1 | 202601 | 100.00
  2 | 202601 | 200.00
(2 rows)

注意这里外层用了 FOREACH 遍历月份数组,内层用 FOR ... IN EXECUTE 遍历动态查询结果——两种循环嵌套是批处理函数的常见骨架。

注意事项 ​

1. 仍然要给大循环加过滤与上限

虽然 FOR 循环内部用游标逐行取、不会把结果集整体读进内存,但整个循环运行在一个事务里。不加过滤地扫描全表会让这个事务长时间运行,带来锁等待与回滚段膨胀。

sql
-- 不推荐:全表扫描,事务时间不可控
-- 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 接收时,查询列必须与表结构一致,否则运行时报错。

sql
-- 错误!只选了两列
-- FOR v_r IN SELECT id, name FROM demo_staff LOOP

-- 正确
FOR v_r IN SELECT * FROM demo_staff LOOP

3. 标量列表的变量个数必须等于列数

多一个少一个都会报错。

sql
-- 错误!两变量对三列
-- 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 LOOP

4. 循环体内部不要依赖 FOUND

FOR/FOREACH 只在退出循环时根据「是否迭代过至少一次」设置 FOUND;循环体里的 UPDATE、PERFORM、SELECT INTO 都会改写它。

sql
-- 错误!循环体里刚做完 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,但要清楚它意味着「不能边消费边追加」。

sql
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 做超大批量处理

行数到几十万、上百万时,单事务时长、锁持有时间与回滚段占用都会成为问题。此时应分批。

sql
-- 不推荐:一次处理全部,事务时间不可控
-- 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 LOOP

7. 动态查询里表名必须用 %I 包裹

否则含大写或特殊字符的表名会出错,更有注入风险。

sql
FOR r IN EXECUTE format('SELECT * FROM %I WHERE status = $1', v_table) USING p_status LOOP

8. 内层 EXIT 只跳一层

嵌套循环里想跳出外层必须打标签。

sql
<<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 不能省

省掉就会退化成字符串拼接,注入风险立刻回来。

sql
-- 错误!值被直接拼进字符串
-- 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 LOOP

10. 循环变量是完整的一行,字段访问需要正确拼写

record 变量在编译期不检查字段名,拼错要到运行期才报错。

sql
-- 运行时才报错:record "v_r" has no field "nmae"
-- RAISE NOTICE '%', v_r.nmae;

最佳实践 ​

1. 能用集合式 SQL 就别用循环

这是最重要的原则。循环是「最后手段」,不是默认选择。

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

结果顺序不确定时,排查问题会非常痛苦。显式排序让行为可复现。

sql
FOR r IN SELECT id, name FROM demo_staff ORDER BY id LOOP

3. 只选需要的列

减少每行传输与反序列化的开销,也避免 %ROWTYPE 的列匹配问题。

sql
-- 推荐
FOR r IN SELECT id, name FROM demo_staff WHERE active ORDER BY id LOOP
-- 不推荐
-- FOR r IN SELECT * FROM demo_staff LOOP

4. 用 %ROWTYPE 换取编译期检查

整行处理时,%ROWTYPE 比 record 更安全,列名拼错会在计划阶段暴露。

sql
DECLARE v_r demo_staff%ROWTYPE;
BEGIN
  FOR v_r IN SELECT * FROM demo_staff LOOP

5. 大批量处理一律分批 + 可中断

给每批一个上限,配合进度日志,运维时可随时取消。

sql
FOR r IN SELECT id FROM demo_msg WHERE pushed = false ORDER BY id LIMIT p_batch LOOP
  ...
END LOOP;

6. 用 CONTINUE WHEN 做前置过滤

把不处理的记录尽早跳过,比在循环体深处写嵌套 IF 更清晰。

sql
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. 循环内避免重复查询同一行

能一次查出来的字段就别在循环里再查一次。

sql
-- 推荐:查询时一并取出
FOR r IN SELECT id, name, dept FROM demo_staff LOOP
  -- r.dept 已经取到,无需再查

8. 动态循环里的标识符统一走 format('%I')

形成肌肉记忆,避免漏掉任何一处。

sql
FOR r IN EXECUTE format('SELECT * FROM %I WHERE id > $1', v_table) USING p_min_id LOOP

9. 给长循环加进度日志

处理上万行时,RAISE NOTICE 每千条一次,能清楚知道卡在哪。

sql
v_n := v_n + 1;
IF v_n % 1000 = 0 THEN
  RAISE NOTICE '已处理 % 行', v_n;
END IF;

10. 需要中途提交时改用游标

FOR ... IN SELECT 的整个循环在一个事务里,无法中途 COMMIT。确实需要分批提交时用显式游标。

sql
-- 需要在遍历中途 COMMIT 时应改用显式游标 + FETCH,而非 FOR ... IN SELECT

11. 把「有副作用」的操作封装成函数

循环体里只留一行函数调用,逻辑集中、便于测试与复用。

sql
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 块让单行失败不影响整批

需要「跳过坏数据、继续处理」时,把循环体包进子块。

sql
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)

参考链接 ​