Skip to content

循环(LOOP / WHILE / FOR) ​

PL/pgSQL 提供了三种循环结构:无条件的 LOOP、带条件的 WHILE,以及基于整数范围的 FOR。它们覆盖了从「无限循环 + 显式退出」到「遍历固定区间」的全部场景。除此之外,FOR ... IN SELECT 用来遍历查询结果、FOREACH 用来遍历数组,这两者放在后续小节单独讲解。

本节聚焦最基本的三种循环,并顺带讲清循环标签(label)与跳出机制,这是把嵌套循环写对的前提。

前置知识

阅读本节前,建议先了解:CASE 语句(简单 CASE / 搜索 CASE)

基础概念 ​

三种循环的一句话定义 ​

结构进入循环的条件是否保证执行零次典型场景
LOOP无条件进入否,至少执行一次需要「先做再判断」或配合 EXIT
WHILE条件为 TRUE 才进入是(条件为假时零次)条件驱动的循环,次数事先未知
FOR(整数)区间非空则进入是(区间为空时零次)已知次数或固定区间

三种循环的执行路径 ​

text
LOOP 结构                      WHILE 结构                   FOR(整数)结构
┌──────────────┐              ┌──────────────┐             ┌──────────────┐
│  LOOP        │              │  WHILE cond  │             │ FOR i IN     │
│    ...       │              │    LOOP      │             │   a..b LOOP  │
│    EXIT WHEN │──────────┐   │      ...     │             │    ...       │
│    ...       │          │   │      ...     │             │  END LOOP;   │
│  END LOOP;   │          │   │    END LOOP; │             └──────────────┘
└──────────────┘          │   └──────────────┘                    │
        ▲                 │          ▲                            │
        └─────────────────┘          └── cond 为假则退出          └── i 超出 b 则退出
        必须自己写 EXIT               每圈开始前重算 cond          进入前算好 a、b、step

两种退出方式 ​

循环体内部有两种「直接跳出来」的手段,本节只讲第一种,第二种在下一节展开:

  • EXIT:立即结束整个循环,跳到 END LOOP; 之后。
  • CONTINUE:跳过本次循环剩余语句,直接进入下一轮。

两者都可以带 WHEN 条件,写成 EXIT WHEN cond; 比 IF cond THEN EXIT; END IF; 更简洁。

循环的三个易错点 ​

  • FOR 的区间边界在进入循环时只求值一次,循环中修改边界变量不会改变循环次数。
  • FOR 的循环变量是循环自己声明的局部副本,在循环体内给它赋值不会改变它的推进(下一轮仍按步长递增)。
  • WHILE 的条件每轮开始时重新求值,所以循环体必须改变条件中涉及的变量,否则就是死循环。

语法与用法 ​

三种循环的完整语法:

text
[ <<label>> ]
LOOP
    语句;
END LOOP [ label ];

[ <<label>> ]
WHILE 布尔表达式 LOOP
    语句;
END LOOP [ label ];

[ <<label>> ]
FOR  变量名 IN [ REVERSE ] 下界 .. 上界 [ BY 步长 ] LOOP
    语句;
END LOOP [ label ];

语法要点拆解:

text
LOOP
 ├── 无条件进入,循环体至少执行一次
 └── 必须有 EXIT / RETURN 之类出口,否则死循环

WHILE 布尔表达式 LOOP
 ├── 布尔表达式为 TRUE 才进入
 ├── 每轮开始前重新求值
 └── 表达式为 NULL 时按 FALSE 处理,不进入

FOR 变量名 IN [REVERSE] 下界 .. 上界 [BY 步长] LOOP
 ├── 变量名由 FOR 自动声明为 integer,作用域仅限循环内
 ├── 下界、上界、步长在进入循环时求值一次
 ├── 默认步长 1;BY 的值必须大于 0,否则报错
 ├── REVERSE 表示从大到小
 └── 区间为空(下界 > 上界)时零次执行

[ <<label>> ]
 ├── 可选,写在结构之前
 ├── 用于 EXIT label / CONTINUE label 跨层跳出
 └── 标签名在块内必须唯一

最短的可用示例:

sql
DO $$
DECLARE
  v_i int := 0;
  v_s int := 0;
BEGIN
  -- LOOP + EXIT WHEN
  LOOP
    v_i := v_i + 1;
    EXIT WHEN v_i > 5;
    v_s := v_s + v_i;
  END LOOP;
  RAISE NOTICE 'LOOP 求和 = %', v_s;

  -- WHILE
  v_i := 0; v_s := 0;
  WHILE v_i < 5 LOOP
    v_i := v_i + 1;
    v_s := v_s + v_i;
  END LOOP;
  RAISE NOTICE 'WHILE 求和 = %', v_s;

  -- FOR
  v_s := 0;
  FOR v_i IN 1..5 LOOP
    v_s := v_s + v_i;
  END LOOP;
  RAISE NOTICE 'FOR 求和 = %', v_s;
END;
$$;

详细说明 ​

LOOP:最原始的无条件循环 ​

LOOP 不带任何条件,进入就执行。它把「什么时候停」完全交给循环体里的 EXIT,因此最适合两种场景:

  • 先执行、再判断:至少要做一次,做完之后才决定是否继续。
  • 复杂退出条件:退出依赖多条语句的计算结果,用 EXIT WHEN 在合适位置表达最清楚。
sql
CREATE OR REPLACE FUNCTION demo_loop_first_try(p_max_try int)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
  v_try  int := 0;
  v_rand numeric;
BEGIN
  LOOP
    v_try := v_try + 1;
    v_rand := random();
    RAISE NOTICE '第 % 次尝试,随机值 = %', v_try, v_rand;

    -- 命中即退出
    EXIT WHEN v_rand > 0.8;
    -- 超过上限也退出
    EXIT WHEN v_try >= p_max_try;
  END LOOP;
  RETURN v_try;
END;
$$;

如果忘了写 EXIT,就会死循环。所幸 PostgreSQL 运行时会持续检查查询取消请求,客户端的 Ctrl+C(取消)仍能中断它:

sql
-- 错误!没有出口,会一直循环
DO $$
BEGIN
  LOOP
    PERFORM pg_sleep(1);
  END LOOP;
END;
$$;

WHILE:条件驱动的循环 ​

WHILE 在每轮开始前判断条件,条件为假就退出。因此它天然适合「不知道要做多少次,但知道做到什么状态为止」的逻辑。

sql
CREATE OR REPLACE FUNCTION demo_while_halve(p_start numeric)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
  v_v   numeric := p_start;
  v_cnt int := 0;
BEGIN
  WHILE v_v > 1 LOOP
    v_v := v_v / 2;
    v_cnt := v_cnt + 1;
  END LOOP;
  RETURN v_cnt;
END;
$$;

SELECT demo_while_halve(1000);   -- 从 1000 折半到不大于 1 需要 10 次

三个必须记住的属性:

  • 可能一次都不执行。若初始条件为假,循环体直接被跳过。
  • 条件是三值逻辑。NULL 会被当作 FALSE 处理,即退出循环,不会报错。
  • 循环体必须推动条件变化。否则就是死循环。
sql
DO $$
DECLARE
  v_cond boolean := NULL;
BEGIN
  -- NULL 条件按 FALSE 处理,循环体不执行
  WHILE v_cond LOOP
    RAISE NOTICE '不会执行';
  END LOOP;
  RAISE NOTICE '循环已跳过';
END;
$$;

FOR:整数区间循环 ​

FOR 是三种循环里最省心的:循环变量、递增、边界判断全部由 PL/pgSQL 负责,不用手写任何推进语句。

sql
DO $$
DECLARE v_i int;
BEGIN
  -- 升序
  FOR v_i IN 1..3 LOOP
    RAISE NOTICE '升序:%', v_i;
  END LOOP;

  -- 降序
  FOR v_i IN REVERSE 3..1 LOOP
    RAISE NOTICE '降序:%', v_i;
  END LOOP;

  -- 自定义步长
  FOR v_i IN 0..10 BY 5 LOOP
    RAISE NOTICE '步长 5:%', v_i;
  END LOOP;
END;
$$;

FOR 的几个关键约束:

  • 下界、上界、步长在进入循环时求值一次,之后不再变化。
  • 步长必须大于零。BY 0 或负数都会在运行时报错(BY value of FOR loop must be greater than zero,SQLSTATE 22023)。想反向遍历用 REVERSE,而不是负步长。
  • 区间为空时零次执行,升序要求 下界 <= 上界,降序(REVERSE)要求 下界 >= 上界。
  • REVERSE 与 BY 可以组合,此时按步长递减。
sql
DO $$
DECLARE v_i int;
BEGIN
  -- 区间为空,循环体一次都不执行
  FOR v_i IN 5..1 LOOP
    RAISE NOTICE '不会打印';
  END LOOP;
  RAISE NOTICE '空区间已跳过';
END;
$$;

FOR 的循环变量:自动声明与只读副本 ​

FOR 的循环变量由 FOR 语句自动声明,类型固定为 integer,作用域仅在循环内。如果外层已有同名变量,FOR 会把它遮蔽。

更关键的一点:循环变量在循环体内是只读的副本。给它赋值不会影响循环的推进。如果你需要「跳过若干次」,应该用 CONTINUE 或改上界,而不是修改循环变量。

sql
DO $$
DECLARE
  v_i int := 999;      -- 外层变量
  v_seen int := 0;
BEGIN
  FOR v_i IN 1..3 LOOP
    -- 这里的 v_i 是 FOR 自动声明的局部变量,遮蔽了外层的 999
    RAISE NOTICE '循环内 v_i = %', v_i;
    v_i := 100;        -- 赋值无效:下一轮仍按 1,2,3 推进
  END LOOP;
  RAISE NOTICE '循环后 v_i = %(外层变量未被改动)', v_i;
END;
$$;
text
NOTICE:  循环内 v_i = 1
NOTICE:  循环内 v_i = 2
NOTICE:  循环内 v_i = 3
NOTICE:  循环后 v_i = 999(外层变量未被改动)

REVERSE 与 BY 的组合规则 ​

REVERSE 只负责改变推进方向:默认是「每轮加上步长」,加了 REVERSE 就变成「每轮减去步长」。下界、上界、步长三者都在进入循环时求值一次。

写法遍历顺序说明
FOR i IN 1..10 BY 31, 4, 7, 10每轮加 3
FOR i IN REVERSE 10..1 BY 210, 8, 6, 4, 2每轮减 2
FOR i IN 1..101…10默认步长 1
FOR i IN REVERSE 1..1010…1降序

当区间为空时(升序里下界大于上界,或 REVERSE 里下界小于上界),循环体一次都不执行,也不会报错。

sql
DO $$
DECLARE v_i int;
BEGIN
  -- REVERSE + BY:从 10 开始每次减 2
  FOR v_i IN REVERSE 10..1 BY 2 LOOP
    RAISE NOTICE 'v_i = %', v_i;   -- 10, 8, 6, 4, 2
  END LOOP;
END;
$$;

循环标签:跨层退出与限定的前提 ​

多了一层嵌套循环后,「跳出哪一层」就成了问题。默认的 EXIT 只跳出最内层;若想跳出外层,必须给外层加标签(label),并在 EXIT 中指名。

标签写在结构之前,用 <<...>> 包裹:

sql
CREATE OR REPLACE FUNCTION demo_label_exit()
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
  v_i int;
  v_j int;
BEGIN
  <<outer_loop>>
  FOR v_i IN 1..5 LOOP
    FOR v_j IN 1..5 LOOP
      IF v_i * v_j = 6 THEN
        RAISE NOTICE '命中 i=% j=%', v_i, v_j;
        EXIT outer_loop;      -- 一次跳出两层
      END IF;
    END LOOP;
  END LOOP outer_loop;

  RETURN format('结束于 i=% , j=%', v_i, v_j);
END;
$$;

对比不带标签的写法,差别很明显:

sql
DO $$
DECLARE v_i int; v_j int; v_hits int := 0;
BEGIN
  FOR v_i IN 1..3 LOOP
    FOR v_j IN 1..3 LOOP
      EXIT;                 -- 只跳出内层,外层继续
      v_hits := v_hits + 1; -- 永远到不了
    END LOOP;
  END LOOP;
  RAISE NOTICE '内层每次都只跑一圈';
END;
$$;

标签也适用于块,不只适用于循环 ​

<<label>> 可以加在 BEGIN 块上,此时 EXIT label 会跳出整个块。这在「提前结束一段计算」时很有用,等价于一次受控的 GOTO。

sql
CREATE OR REPLACE FUNCTION app_validate(p_a int, p_b int)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
  v_msg text := '通过';
BEGIN
  <<validate_block>>
  BEGIN
    IF p_a IS NULL THEN
      v_msg := 'a 为空';
      EXIT validate_block;
    END IF;
    IF p_a <= 0 THEN
      v_msg := 'a 必须为正';
      EXIT validate_block;
    END IF;
    IF p_b IS NULL OR p_b <= p_a THEN
      v_msg := 'b 必须大于 a';
      EXIT validate_block;
    END IF;
    -- 全部通过则继续追加
    v_msg := format('通过:a=%s b=%s', p_a, p_b);
  END validate_block;

  RETURN v_msg;
END;
$$;

循环与查询的组合:先读后写 ​

FOR 循环最常见的用法,是配合「先查出一条待处理清单,再逐条处理」。此处先用最简单的做法:把要处理的 id 收集到数组,再用整数 FOR 遍历下标。更直接的「遍历查询结果」写法在下一节 FOR ... IN SELECT 中介绍。

sql
CREATE OR REPLACE FUNCTION demo_loop_over_ids(p_limit int)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
  v_ids  bigint[];
  v_i    int;
  v_cnt  int := 0;
BEGIN
  -- 一次查出所有待处理 id
  SELECT array_agg(id) INTO v_ids
    FROM (SELECT id FROM demo_task WHERE status = 'new' ORDER BY id LIMIT p_limit) t;

  IF v_ids IS NULL THEN          -- 没有待处理行
    RETURN 0;
  END IF;

  FOR v_i IN 1..array_length(v_ids, 1) LOOP
    UPDATE demo_task SET status = 'doing' WHERE id = v_ids[v_i];
    v_cnt := v_cnt + 1;
  END LOOP;

  RETURN v_cnt;
END;
$$;

循环的边界:什么时候不该用循环 ​

PL/pgSQL 的循环本质是「一次一行、一次一条 SQL」,在大数据量下远不如集合式 SQL 高效。判断标准很简单:

  • 能用一条 UPDATE ... FROM / INSERT ... SELECT 表达的,不要写循环。
  • 循环适合「每行都需要不同处理逻辑」或「需要调用外部过程」的场景。
  • 循环内的 SQL 语句会被反复规划执行,全表级操作应尽量批量化。
sql
-- 不推荐:循环逐行更新,N 行 = N 条 UPDATE
-- FOR r IN SELECT id FROM t WHERE flag = false LOOP
--   UPDATE t SET flag = true WHERE id = r.id;
-- END LOOP;

-- 推荐:一条语句搞定
UPDATE demo_task SET status = 'done' WHERE status = 'doing';

实战示例 ​

场景一:用 WHILE 做分页批处理 ​

需求:demo_msg 表里有大量未推送的消息,需要分批(每批 500 条)标记为已推送,避免一次锁太多行;同时要防止无限循环。

建表与造数:

sql
CREATE TABLE IF NOT EXISTS demo_msg(
  id        bigserial PRIMARY KEY,
  user_id   bigint NOT NULL,
  content   text   NOT NULL,
  pushed    boolean NOT NULL DEFAULT false,
  pushed_at timestamptz
);

INSERT INTO demo_msg(user_id, content)
SELECT (i % 100) + 1, 'msg-' || i
FROM generate_series(1, 2000) AS i;

建函数:

sql
CREATE OR REPLACE FUNCTION app_push_batch(p_batch_size int DEFAULT 500, p_max_round int DEFAULT 100)
RETURNS TABLE(round_no int, affected bigint, total bigint)
LANGUAGE plpgsql AS $$
DECLARE
  v_round   int := 0;
  v_affected bigint;
  v_total   bigint := 0;
BEGIN
  WHILE v_round < p_max_round LOOP
    v_round := v_round + 1;

    WITH picked AS (
      SELECT id FROM demo_msg
       WHERE pushed = false
       ORDER BY id
       LIMIT p_batch_size
       FOR UPDATE SKIP LOCKED
    )
    UPDATE demo_msg m
       SET pushed = true, pushed_at = now()
      FROM picked p
     WHERE m.id = p.id;

    GET DIAGNOSTICS v_affected = ROW_COUNT;
    v_total := v_total + v_affected;

    round_no  := v_round;
    affected  := v_affected;
    total     := v_total;
    RETURN NEXT;

    -- 没有可更新的行了,提前收工
    EXIT WHEN v_affected < p_batch_size;
  END LOOP;
END;
$$;

调用与输出:

sql
SELECT * FROM app_push_batch(500, 10);
text
-- round_no/affected/total: 1/500/500, 2/500/1000, 3/500/1500, 4/500/2000, 5/0/2000

WHILE 在这里承担了「批次数未知、跑到没数据为止」的角色,p_max_round 是兜底保险,EXIT WHEN 是正常出口。注意每批都用了 FOR UPDATE SKIP LOCKED,多个 worker 并发跑时不会互相阻塞。

场景二:用带标签的双层循环生成排班矩阵 ​

需求:为每位员工生成一周(7 天)的排班占位记录,但一旦遇到员工已离职就停止该员工的生成,并跳过 2026-01-01 这个法定假日。

建表与造数:

sql
CREATE TABLE IF NOT EXISTS demo_staff(
  id       bigserial PRIMARY KEY,
  name     text NOT NULL,
  active   boolean NOT NULL DEFAULT true
);
CREATE TABLE IF NOT EXISTS demo_shift(
  staff_id bigint NOT NULL,
  work_day date   NOT NULL,
  PRIMARY KEY(staff_id, work_day)
);

INSERT INTO demo_staff(name, active) VALUES
  ('alice', true),
  ('bob',   true),
  ('carol', false),
  ('dave',  true);

建函数:

sql
CREATE OR REPLACE FUNCTION app_gen_shift(p_week_start date)
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
  v_staff   record;
  v_day     int;
  v_date    date;
  v_created int := 0;
BEGIN
  <<staff_loop>>
  FOR v_staff IN
    SELECT id, name, active FROM demo_staff ORDER BY id
  LOOP
    -- 离职员工整段跳过
    IF NOT v_staff.active THEN
      RAISE NOTICE '员工 % 已离职,跳过', v_staff.name;
      CONTINUE staff_loop;
    END IF;

    FOR v_day IN 0..6 LOOP
      v_date := p_week_start + v_day;

      -- 法定假日不排班
      IF v_date = DATE '2026-01-01' THEN
        RAISE NOTICE '员工 % 在 % 休息', v_staff.name, v_date;
        CONTINUE;
      END IF;

      INSERT INTO demo_shift(staff_id, work_day)
      VALUES (v_staff.id, v_date)
      ON CONFLICT DO NOTHING;

      v_created := v_created + 1;
    END LOOP;
  END LOOP staff_loop;

  RETURN v_created;
END;
$$;

调用与输出:

sql
SELECT app_gen_shift(DATE '2025-12-29') AS created;
text
NOTICE:  员工 carol 已离职,跳过
NOTICE:  员工 alice 在 2026-01-01 休息
NOTICE:  员工 bob 在 2026-01-01 休息
NOTICE:  员工 dave 在 2026-01-01 休息
 created
---------
      18
(1 row)

3 位在职员工 × 7 天 − 3 个假日 = 18 条。这里用到了两个标签相关能力:外层循环的 staff_loop 标签配合 CONTINUE staff_loop 实现「跳过整个员工」,内层 CONTINUE(无标签)只跳过当天。

注意事项 ​

1. LOOP 必须有出口

无条件的 LOOP 一旦忘记写 EXIT,就是死循环。

sql
-- 错误!死循环
-- LOOP
--   PERFORM pg_sleep(1);
-- END LOOP;

-- 正确
LOOP
  PERFORM pg_sleep(1);
  EXIT WHEN pg_is_in_recovery();   -- 有明确出口
END LOOP;

2. WHILE 的循环体必须推动条件变化

条件里的变量不变,循环永不退出。

sql
-- 错误!v_i 从未变化
-- WHILE v_i < 10 LOOP
--   RAISE NOTICE '%', v_i;
-- END LOOP;

-- 正确
WHILE v_i < 10 LOOP
  v_i := v_i + 1;
END LOOP;

3. FOR 的循环变量在循环体内赋值无效

它是循环自己维护的只读副本,赋值不会改变推进。

sql
FOR v_i IN 1..10 LOOP
  v_i := 100;     -- 不推荐!无效操作,下一轮仍是 2、3……
END LOOP;

4. 不要修改 FOR 用到的边界变量期望影响循环

边界在进入循环时已经算好。

sql
DO $$
DECLARE v_end int := 3; v_i int;
BEGIN
  FOR v_i IN 1..v_end LOOP
    v_end := 1;   -- 不推荐!不会让循环提前结束
  END LOOP;
END;
$$;

5. 步长必须是正数

BY 的值必须大于零,BY 0 与负数都会运行时报错。要反向遍历请用 REVERSE。

sql
-- 错误!运行时报错:BY value of FOR loop must be greater than zero(SQLSTATE 22023)
-- FOR v_i IN 1..10 BY 0 LOOP ... END LOOP;

-- 推荐:反向遍历用 REVERSE,步长仍写正数
FOR v_i IN REVERSE 10..1 BY 2 LOOP ... END LOOP;

6. EXIT 默认只跳出最内层

要跳多层必须给外层加标签。

sql
-- 不推荐:期望跳出两层却只跳了一层
-- FOR i IN 1..3 LOOP
--   FOR j IN 1..3 LOOP
--     EXIT;
--   END LOOP;
-- END LOOP;

-- 正确
<<outer>>
FOR i IN 1..3 LOOP
  FOR j IN 1..3 LOOP
    EXIT outer;
  END LOOP;
END LOOP outer;

7. 标签名不能与块内已有标签重复

同一层级的标签必须唯一,否则报错。

sql
-- 错误!同一块内标签重名
-- <<lbl>> FOR i IN 1..2 LOOP
--   <<lbl>> FOR j IN 1..2 LOOP   -- 冲突
--   END LOOP lbl;
-- END LOOP lbl;

8. 循环内执行 SQL 的性能陷阱

N 次循环可能意味着 N 次语句执行与 N 次规划,行数多时非常慢。

sql
-- 不推荐:逐行 INSERT
-- FOR r IN SELECT * FROM src LOOP
--   INSERT INTO dst VALUES (r.a, r.b);
-- END LOOP;

-- 推荐
INSERT INTO demo_shift(staff_id, work_day)
SELECT id, CURRENT_DATE FROM demo_staff WHERE active;

9. 循环里的异常会中断整个循环

如果不处理,某一行出错会让整段循环回滚(在事务语义下)。需要「出错就跳过」时必须包 BEGIN ... EXCEPTION。

sql
FOR r IN SELECT id, val FROM demo_src LOOP
  BEGIN
    PERFORM app_process(r.id, r.val);
  EXCEPTION
    WHEN others THEN
      RAISE WARNING '处理 id=% 失败:%', r.id, SQLERRM;
      -- 继续下一行
  END;
END LOOP;

10. 慎用超长 FOR 区间

FOR v_i IN 1..100000000 会真的跑一亿次,而不是「有优化」。区间大小就是循环次数。

sql
-- 危险:会执行一亿次
-- FOR v_i IN 1..100000000 LOOP ... END LOOP;

最佳实践 ​

1. 优先用最贴合语义的循环类型

「已知次数」用 FOR,条件驱动用 WHILE,先做再判断用 LOOP。选对类型,代码可读性立刻提升。

sql
-- 明确次数 → FOR
FOR v_i IN 1..p_retry_cnt LOOP ... END LOOP;
-- 条件驱动 → WHILE
WHILE v_remaining > 0 LOOP ... END LOOP;
-- 至少一次 → LOOP + EXIT
LOOP ... EXIT WHEN ok; END LOOP;

2. 用 EXIT WHEN 而不是 IF ... EXIT

EXIT WHEN 是专门为此设计的语法,更短更清晰。

sql
-- 推荐
EXIT WHEN v_cnt >= p_max;
-- 不推荐
-- IF v_cnt >= p_max THEN EXIT; END IF;

3. 每个循环都加一个「最大轮次」保险

业务循环最怕因为数据异常导致死循环,兜底计数是最廉价的保护。

sql
WHILE v_continue LOOP
  v_round := v_round + 1;
  EXIT WHEN v_round > p_max_round;    -- 保险丝
  ...
END LOOP;

4. 给嵌套循环加标签,即使暂时用不上

嵌套层数 ≥ 2 时提前命名外层标签,后续需要跨层跳转时不用回头改结构。

sql
<<rows_loop>>
FOR v_row IN SELECT ... LOOP
  <<cols_loop>>
  FOR v_col IN SELECT ... LOOP
    ...
  END LOOP cols_loop;
END LOOP rows_loop;

5. 循环变量不要与外层变量同名

FOR 会静默遮蔽外层同名变量,藏着难查的 bug。用有区分度的名字。

sql
-- 推荐
FOR v_idx IN 1..n LOOP ... END LOOP;
-- 不推荐:遮蔽了外层的 v_i
-- DECLARE v_i int; BEGIN FOR v_i IN 1..n LOOP ...

6. 尽量把循环内的 SQL 批量化

把「循环 + 单行 DML」改写为「一条集合 DML」,通常是数量级的性能差别。

sql
-- 推荐:一次更新所有满足条件的行
UPDATE demo_msg SET pushed = true WHERE pushed = false AND id <= p_upper;

7. 大数据量循环要控制事务规模

函数内不能 COMMIT,但存储过程可以 COMMIT;在批量场景下用过程分批提交,或用 GET DIAGNOSTICS 判断批次是否收工(比再查一次 count(*) 便宜)。

sql
-- 存储过程内可分批提交(函数内不可)
-- CREATE PROCEDURE app_push_all() LANGUAGE plpgsql AS $$
-- BEGIN
--   LOOP ... COMMIT; EXIT WHEN done; END LOOP;
-- END $$;

-- 函数内:用影响行数判断是否收工
GET DIAGNOSTICS v_affected = ROW_COUNT;
EXIT WHEN v_affected = 0;

8. 用 CONTINUE 表达「跳过」,而不是深嵌套 IF

扁平化的循环体更易读。

sql
-- 推荐
FOR r IN SELECT * FROM demo_staff LOOP
  CONTINUE WHEN NOT r.active;
  ...
END LOOP;

9. 循环里避免重复计算不变表达式

把循环无关的计算提到循环外。

sql
-- 推荐:算一次
v_bonus := app_calc_bonus_rate(p_level);
FOR r IN SELECT * FROM demo_staff LOOP
  v_pay := r.salary * (1 + v_bonus);
END LOOP;

10. 用 RAISE NOTICE 或 RAISE DEBUG 记录循环进度

长循环出问题时,有进度日志会大幅缩短排查时间。

sql
IF v_i % 1000 = 0 THEN
  RAISE NOTICE '已处理 % 条', v_i;
END IF;

11. 循环内调用函数要注意开销

循环次数 × 单次函数调用开销,可能远超预期。能内联的简单逻辑不要封装成函数调用。

sql
-- 循环里调用函数的成本 = 调用次数 × 调用开销
-- 对纯计算类逻辑,直接内联表达式往往更快

参考链接 ​