循环(LOOP / WHILE / FOR)
PL/pgSQL 提供了三种循环结构:无条件的 LOOP、带条件的 WHILE,以及基于整数范围的 FOR。它们覆盖了从「无限循环 + 显式退出」到「遍历固定区间」的全部场景。除此之外,FOR ... IN SELECT 用来遍历查询结果、FOREACH 用来遍历数组,这两者放在后续小节单独讲解。
本节聚焦最基本的三种循环,并顺带讲清循环标签(label)与跳出机制,这是把嵌套循环写对的前提。
前置知识
阅读本节前,建议先了解:CASE 语句(简单 CASE / 搜索 CASE)
基础概念
三种循环的一句话定义
| 结构 | 进入循环的条件 | 是否保证执行零次 | 典型场景 |
|---|---|---|---|
LOOP | 无条件进入 | 否,至少执行一次 | 需要「先做再判断」或配合 EXIT |
WHILE | 条件为 TRUE 才进入 | 是(条件为假时零次) | 条件驱动的循环,次数事先未知 |
FOR(整数) | 区间非空则进入 | 是(区间为空时零次) | 已知次数或固定区间 |
三种循环的执行路径
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的条件每轮开始时重新求值,所以循环体必须改变条件中涉及的变量,否则就是死循环。
语法与用法
三种循环的完整语法:
[ <<label>> ]
LOOP
语句;
END LOOP [ label ];
[ <<label>> ]
WHILE 布尔表达式 LOOP
语句;
END LOOP [ label ];
[ <<label>> ]
FOR 变量名 IN [ REVERSE ] 下界 .. 上界 [ BY 步长 ] LOOP
语句;
END LOOP [ label ];语法要点拆解:
LOOP
├── 无条件进入,循环体至少执行一次
└── 必须有 EXIT / RETURN 之类出口,否则死循环
WHILE 布尔表达式 LOOP
├── 布尔表达式为 TRUE 才进入
├── 每轮开始前重新求值
└── 表达式为 NULL 时按 FALSE 处理,不进入
FOR 变量名 IN [REVERSE] 下界 .. 上界 [BY 步长] LOOP
├── 变量名由 FOR 自动声明为 integer,作用域仅限循环内
├── 下界、上界、步长在进入循环时求值一次
├── 默认步长 1;BY 的值必须大于 0,否则报错
├── REVERSE 表示从大到小
└── 区间为空(下界 > 上界)时零次执行
[ <<label>> ]
├── 可选,写在结构之前
├── 用于 EXIT label / CONTINUE label 跨层跳出
└── 标签名在块内必须唯一最短的可用示例:
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在合适位置表达最清楚。
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(取消)仍能中断它:
-- 错误!没有出口,会一直循环
DO $$
BEGIN
LOOP
PERFORM pg_sleep(1);
END LOOP;
END;
$$;WHILE:条件驱动的循环
WHILE 在每轮开始前判断条件,条件为假就退出。因此它天然适合「不知道要做多少次,但知道做到什么状态为止」的逻辑。
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处理,即退出循环,不会报错。 - 循环体必须推动条件变化。否则就是死循环。
DO $$
DECLARE
v_cond boolean := NULL;
BEGIN
-- NULL 条件按 FALSE 处理,循环体不执行
WHILE v_cond LOOP
RAISE NOTICE '不会执行';
END LOOP;
RAISE NOTICE '循环已跳过';
END;
$$;FOR:整数区间循环
FOR 是三种循环里最省心的:循环变量、递增、边界判断全部由 PL/pgSQL 负责,不用手写任何推进语句。
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,SQLSTATE22023)。想反向遍历用REVERSE,而不是负步长。 - 区间为空时零次执行,升序要求
下界 <= 上界,降序(REVERSE)要求下界 >= 上界。 REVERSE与BY可以组合,此时按步长递减。
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 或改上界,而不是修改循环变量。
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;
$$;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 3 | 1, 4, 7, 10 | 每轮加 3 |
FOR i IN REVERSE 10..1 BY 2 | 10, 8, 6, 4, 2 | 每轮减 2 |
FOR i IN 1..10 | 1…10 | 默认步长 1 |
FOR i IN REVERSE 1..10 | 10…1 | 降序 |
当区间为空时(升序里下界大于上界,或 REVERSE 里下界小于上界),循环体一次都不执行,也不会报错。
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 中指名。
标签写在结构之前,用 <<...>> 包裹:
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;
$$;对比不带标签的写法,差别很明显:
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。
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 中介绍。
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 语句会被反复规划执行,全表级操作应尽量批量化。
-- 不推荐:循环逐行更新,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 条)标记为已推送,避免一次锁太多行;同时要防止无限循环。
建表与造数:
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;建函数:
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;
$$;调用与输出:
SELECT * FROM app_push_batch(500, 10);-- round_no/affected/total: 1/500/500, 2/500/1000, 3/500/1500, 4/500/2000, 5/0/2000WHILE 在这里承担了「批次数未知、跑到没数据为止」的角色,p_max_round 是兜底保险,EXIT WHEN 是正常出口。注意每批都用了 FOR UPDATE SKIP LOCKED,多个 worker 并发跑时不会互相阻塞。
场景二:用带标签的双层循环生成排班矩阵
需求:为每位员工生成一周(7 天)的排班占位记录,但一旦遇到员工已离职就停止该员工的生成,并跳过 2026-01-01 这个法定假日。
建表与造数:
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);建函数:
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;
$$;调用与输出:
SELECT app_gen_shift(DATE '2025-12-29') AS created;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,就是死循环。
-- 错误!死循环
-- LOOP
-- PERFORM pg_sleep(1);
-- END LOOP;
-- 正确
LOOP
PERFORM pg_sleep(1);
EXIT WHEN pg_is_in_recovery(); -- 有明确出口
END LOOP;2. WHILE 的循环体必须推动条件变化
条件里的变量不变,循环永不退出。
-- 错误!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 的循环变量在循环体内赋值无效
它是循环自己维护的只读副本,赋值不会改变推进。
FOR v_i IN 1..10 LOOP
v_i := 100; -- 不推荐!无效操作,下一轮仍是 2、3……
END LOOP;4. 不要修改 FOR 用到的边界变量期望影响循环
边界在进入循环时已经算好。
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。
-- 错误!运行时报错: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 默认只跳出最内层
要跳多层必须给外层加标签。
-- 不推荐:期望跳出两层却只跳了一层
-- 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. 标签名不能与块内已有标签重复
同一层级的标签必须唯一,否则报错。
-- 错误!同一块内标签重名
-- <<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 次规划,行数多时非常慢。
-- 不推荐:逐行 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。
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 会真的跑一亿次,而不是「有优化」。区间大小就是循环次数。
-- 危险:会执行一亿次
-- FOR v_i IN 1..100000000 LOOP ... END LOOP;最佳实践
1. 优先用最贴合语义的循环类型
「已知次数」用 FOR,条件驱动用 WHILE,先做再判断用 LOOP。选对类型,代码可读性立刻提升。
-- 明确次数 → 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 是专门为此设计的语法,更短更清晰。
-- 推荐
EXIT WHEN v_cnt >= p_max;
-- 不推荐
-- IF v_cnt >= p_max THEN EXIT; END IF;3. 每个循环都加一个「最大轮次」保险
业务循环最怕因为数据异常导致死循环,兜底计数是最廉价的保护。
WHILE v_continue LOOP
v_round := v_round + 1;
EXIT WHEN v_round > p_max_round; -- 保险丝
...
END LOOP;4. 给嵌套循环加标签,即使暂时用不上
嵌套层数 ≥ 2 时提前命名外层标签,后续需要跨层跳转时不用回头改结构。
<<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。用有区分度的名字。
-- 推荐
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」,通常是数量级的性能差别。
-- 推荐:一次更新所有满足条件的行
UPDATE demo_msg SET pushed = true WHERE pushed = false AND id <= p_upper;7. 大数据量循环要控制事务规模
函数内不能 COMMIT,但存储过程可以 COMMIT;在批量场景下用过程分批提交,或用 GET DIAGNOSTICS 判断批次是否收工(比再查一次 count(*) 便宜)。
-- 存储过程内可分批提交(函数内不可)
-- 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
扁平化的循环体更易读。
-- 推荐
FOR r IN SELECT * FROM demo_staff LOOP
CONTINUE WHEN NOT r.active;
...
END LOOP;9. 循环里避免重复计算不变表达式
把循环无关的计算提到循环外。
-- 推荐:算一次
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 记录循环进度
长循环出问题时,有进度日志会大幅缩短排查时间。
IF v_i % 1000 = 0 THEN
RAISE NOTICE '已处理 % 条', v_i;
END IF;11. 循环内调用函数要注意开销
循环次数 × 单次函数调用开销,可能远超预期。能内联的简单逻辑不要封装成函数调用。
-- 循环里调用函数的成本 = 调用次数 × 调用开销
-- 对纯计算类逻辑,直接内联表达式往往更快下一节