遍历数组(FOREACH)
上一节用 FOR ... IN SELECT 遍历查询结果,这一节换一种数据源:数组。PL/pgSQL 提供了 FOREACH 语句,专门用来按顺序遍历数组元素。它在「批量参数逐项处理」「二维矩阵遍历」「JSON 数组展开」等场景下比展开成查询再遍历更直接。
本节讲解 FOREACH 的语法、一维与多维(SLICE)遍历、NULL 数组与空数组的处理,以及它与 unnest() 方案的取舍。
前置知识
阅读本节前,建议先了解:遍历查询结果(FOR ... IN SELECT)
基础概念
为什么需要 FOREACH
数组在 PostgreSQL 里是一等公民:函数参数可以收数组,array_agg() 可以产出数组,text[] 是批处理接口的常见入参类型。拿到一个数组后,最常见的需求就是「逐个元素处理」。
用整数 FOR 也可以做到,但要手动算下标:
-- 手写下标遍历:啰嗦且容易越界
FOR v_i IN 1..array_length(v_ids, 1) LOOP
v_id := v_ids[v_i];
...
END LOOP;FOREACH 把这件事简化为一行:
-- FOREACH:直接拿到元素
FOREACH v_id IN ARRAY v_ids LOOP
...
END LOOP;三种「遍历」的对比
| 语句 | 数据源 | 每轮拿到什么 | 典型场景 |
|---|---|---|---|
FOR i IN 1..n | 整数区间 | 整数 | 固定次数循环 |
FOR r IN SELECT ... | 查询结果集 | 一行记录 | 遍历表数据 |
FOREACH x IN ARRAY arr | 数组 | 一个元素 | 遍历数组、批量参数 |
下标从 1 开始
PostgreSQL 数组默认下标下界是 1(可以自定义,但默认是 1)。FOREACH 按数组的实际下标顺序遍历,与下界是多少无关。
数组 v_arr := ARRAY['a','b','c']
下标: 1 2 3
FOREACH 顺序: 'a' → 'b' → 'c'NULL 与空数组
两个容易混淆的情况:
NULL数组:根本不进入循环体,一次都不执行。- 空数组:同样不进入循环体,一次都不执行。
两者在 FOREACH 下表现一致(都是零次),但含义不同——如果需要区分「没传」和「传了空」,要在循环外先判断 IS NULL。
语法与用法
完整语法:
[ <<label>> ]
FOREACH 目标 [ SLICE 数字 ] IN ARRAY 数组表达式 LOOP
语句;
END LOOP [ label ];语法要点:
FOREACH 目标 [ SLICE n ] IN ARRAY 数组表达式 LOOP
│ │ │
│ │ └── 任意求值为数组的表达式
│ │ (变量、函数调用、ARRAY[...] 字面量)
│ │
│ └── 可选。n = 1 时目标接收「子数组」而非单个元素
│ 用于一次取出一维切片
│
└── 单个变量。类型与数组元素类型一致
(多维遍历时可以是数组类型)
END LOOP [ label ];最小示例:
DO $$
DECLARE
v_name text;
v_arr text[] := ARRAY['alice', 'bob', 'carol'];
BEGIN
FOREACH v_name IN ARRAY v_arr LOOP
RAISE NOTICE '遍历到:%', v_name;
END LOOP;
END;
$$;NOTICE: 遍历到:alice
NOTICE: 遍历到:bob
NOTICE: 遍历到:carol元素类型可以是任意类型,包括复合类型与 record:
DO $$
DECLARE
v_n int;
v_arr int[] := ARRAY[1, 2, 3];
BEGIN
FOREACH v_n IN ARRAY v_arr LOOP
RAISE NOTICE '整数元素:%', v_n;
END LOOP;
END;
$$;详细说明
一维数组遍历:最基本形态
一维数组是最常见的用法。目标变量的类型要和数组元素类型一致,否则会在运行时报类型不匹配错误。
CREATE OR REPLACE FUNCTION app_sum_ids(p_ids bigint[])
RETURNS numeric LANGUAGE plpgsql AS $$
DECLARE
v_id bigint;
v_sum numeric := 0;
v_cnt int := 0;
BEGIN
FOREACH v_id IN ARRAY p_ids LOOP
v_sum := v_sum + app_order_total(v_id); -- 逐单累加
v_cnt := v_cnt + 1;
END LOOP;
RAISE NOTICE '共处理 % 个订单', v_cnt;
RETURN v_sum;
END;
$$;FOREACH 不提供下标,所以如果你既需要元素又需要位置,有两条路:
- 自己维护计数器:简单直接,多数情况下够用。
- 改用整数
FOR+ 下标:需要真正用到原始下标时用。
DO $$
DECLARE
v_v text;
v_idx int := 0;
v_arr text[] := ARRAY['a', 'b', 'c'];
BEGIN
FOREACH v_v IN ARRAY v_arr LOOP
v_idx := v_idx + 1;
RAISE NOTICE '第 % 个元素 = %', v_idx, v_v;
END LOOP;
END;
$$;处理 NULL 数组与空数组
FOREACH 对 NULL 数组和空数组都是「零次循环」。这一点与很多人的直觉不同——它不会抛异常。
DO $$
DECLARE
v_v text;
v_n int;
v_arr text[] := NULL; -- NULL 数组
v_emp text[] := ARRAY[]::text[]; -- 空数组
BEGIN
FOREACH v_v IN ARRAY v_arr LOOP
v_n := v_n + 1;
END LOOP;
RAISE NOTICE 'NULL 数组循环次数 = %', coalesce(v_n, 0);
FOREACH v_v IN ARRAY v_emp LOOP
RAISE NOTICE '空数组不会打印';
END LOOP;
RAISE NOTICE '空数组处理完毕';
END;
$$;如果需要区分两种情况,要在进入循环前判断:
IF p_ids IS NULL THEN
RAISE NOTICE '参数未提供';
RETURN 0;
ELSIF array_length(p_ids, 1) IS NULL THEN
RAISE NOTICE '参数为空数组';
RETURN 0;
END IF;array_length(空数组, 1) 返回 NULL 而不是 0,这是一个常见陷阱:
SELECT array_length(ARRAY[]::int[], 1); -- NULL(不是 0)
SELECT cardinality(ARRAY[]::int[]); -- 0(这个才是元素总数)cardinality() 是统计数组元素个数的正确函数,比 array_length 更稳健。
SLICE:一次取出一个子数组
不加 SLICE 时,目标变量接收单个元素;加了 SLICE n 后,目标变量接收的是每次 n 维的切片,因此此时目标变量必须是数组类型(官方原文:the target variable must be an array, and it receives successive slices of the array value, where each slice is of the number of dimensions specified by SLICE)。
DO $$
DECLARE
v_slice int[]; -- 目标必须是数组
v_m int[][] := ARRAY[[1,2,3],[4,5,6],[7,8,9]];
BEGIN
FOREACH v_slice SLICE 1 IN ARRAY v_m LOOP
RAISE NOTICE 'SLICE 1 取出:%', v_slice; -- 一维切片
END LOOP;
END;
$$;NOTICE: SLICE 1 取出:{1,2,3}
NOTICE: SLICE 1 取出:{4,5,6}
NOTICE: SLICE 1 取出:{7,8,9}SLICE n 中的 n 表示「一次取出的维度数」,接收变量必须是能容纳 n 维切片的数组类型:
| 数组维度 | SLICE 值 | 目标变量类型 | 每轮拿到 |
|---|---|---|---|
| 1 维 | 不写 | 元素类型(标量) | 一个元素 |
| 2 维 | 不写 | 元素类型(标量) | 一个元素(按存储顺序摊平) |
| 2 维 | SLICE 1 | 一维数组(如 int[]) | 一整行切片 |
| 3 维 | SLICE 1 | 二维数组(如 int[][]) | 一层二维切片 |
| 3 维 | SLICE 2 | 一维数组(如 int[]) | 一个二维切片 |
关键点:目标变量永远是数组,且数组维度必须与 SLICE 的值一致。如果误把目标声明成标量,会得到类型不匹配的错误。
不写 SLICE 时,FOREACH 会逐元素遍历,多维数组被按存储顺序摊平成标量序列:
DO $$
DECLARE
v_v int;
v_m int[][] := ARRAY[[1,2],[3,4]];
BEGIN
-- 不写 SLICE:按存储顺序逐元素,输出 1,2,3,4
FOREACH v_v IN ARRAY v_m LOOP
RAISE NOTICE '扁平元素:%', v_v;
END LOOP;
END;
$$;与 unnest() 方案的取舍
把数组展开成结果集,再用 FOR ... IN SELECT 遍历,也是一种常见做法:
-- 方案 B:先 unnest 再遍历
FOR v_row IN
SELECT value, ordinality FROM unnest(v_ids) WITH ORDINALITY AS t(value, ordinality)
LOOP
RAISE NOTICE '第 % 个 id = %', v_row.ordinality, v_row.value;
END LOOP;两种方案的对比:
| 维度 | FOREACH | unnest() + FOR ... IN SELECT |
|---|---|---|
| 语法简洁度 | 更简洁 | 略啰嗦 |
| 能否拿到下标 | 不能(需自维护) | 能(WITH ORDINALITY) |
| 能否顺便 JOIN | 不能 | 能(unnest 可与其他表连接) |
| 结果集是否物化 | 数组已在内存中 | 会多一层结果集 |
| 适用场景 | 纯数组逐项处理 | 需要下标或需要与表连接 |
选择原则:只要不需要下标、不需要 JOIN,就用 FOREACH。
-- 推荐:纯遍历
FOREACH v_id IN ARRAY p_ids LOOP
PERFORM app_touch(v_id);
END LOOP;
-- 需要序号或要与表连接时改用 unnest
FOR v_r IN
SELECT t.value, o.status
FROM unnest(p_ids) AS t(value)
JOIN demo_order o ON o.id = t.value
LOOP
...
END LOOP;遍历复合类型数组
数组元素可以是复合类型,这在「一次传入一批结构化参数」时很有用。元素是复合类型时,目标变量要声明成对应类型(或 record)。
CREATE TYPE demo_stock_move AS (sku text, qty int);
CREATE OR REPLACE FUNCTION app_apply_moves(p_moves demo_stock_move[])
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_m demo_stock_move;
v_cnt int := 0;
BEGIN
FOREACH v_m IN ARRAY p_moves LOOP
-- 字段访问用点号
UPDATE demo_stock
SET qty = qty + v_m.qty
WHERE sku = v_m.sku;
IF NOT FOUND THEN
RAISE NOTICE 'SKU % 不存在,跳过', v_m.sku;
CONTINUE;
END IF;
v_cnt := v_cnt + 1;
END LOOP;
RETURN v_cnt;
END;
$$;调用时用 ARRAY[...] 构造,或用 array_agg 从查询结果得到:
SELECT app_apply_moves(ARRAY[
ROW('SKU-1', 10)::demo_stock_move,
ROW('SKU-2', -3)::demo_stock_move
]);循环控制:EXIT 与 CONTINUE
FOREACH 支持全部循环控制语句,用法与其它循环一致,包括标签跳转。
CREATE OR REPLACE FUNCTION app_first_positive(p_nums int[])
RETURNS int LANGUAGE plpgsql AS $$
DECLARE
v_n int;
BEGIN
FOREACH v_n IN ARRAY p_nums LOOP
CONTINUE WHEN v_n <= 0; -- 跳过非正数
RETURN v_n; -- 找到即返回
END LOOP;
RETURN NULL;
END;
$$;
SELECT app_first_positive(ARRAY[-1, 0, 5, 8]); -- 5嵌套数组遍历时,标签让跨层跳出变得清晰:
<<outer_arr>>
FOREACH v_a IN ARRAY p_rows LOOP
FOREACH v_b IN ARRAY p_cols LOOP
IF v_a = v_b THEN
EXIT outer_arr; -- 跳到两层之外
END IF;
END LOOP;
END LOOP outer_arr;数组下界不是 1 的情况
PostgreSQL 数组的下界可以不是 1,例如 SELECT '[0:2]={10,20,30}'::int[] 的下标是 0、1、2。FOREACH 按实际存储顺序遍历,与下界无关;但如果你改用整数 FOR + 下标,就必须用 array_lower() / array_upper() 取实际边界,不能硬编码 1。
DO $$
DECLARE
v_arr int[] := '[0:2]={10,20,30}'::int[];
v_v int;
BEGIN
RAISE NOTICE '下界 = % , 上界 = %', array_lower(v_arr, 1), array_upper(v_arr, 1);
-- FOREACH 与下界无关,照样全部遍历
FOREACH v_v IN ARRAY v_arr LOOP
RAISE NOTICE '元素:%', v_v; -- 10, 20, 30
END LOOP;
END;
$$;用 FOREACH 就完全不必操心下界问题,这是它的一个隐性优势。
性能特征与何时该换成集合式 SQL
FOREACH 在 PL/pgSQL 内部遍历内存中的数组,循环本身开销很小。真正的成本在循环体里的每条 SQL:循环 N 次就是 N 条语句。
-- 不推荐:N 次 UPDATE
FOREACH v_id IN ARRAY p_ids LOOP
UPDATE demo_order SET status = 'done' WHERE id = v_id;
END LOOP;
-- 推荐:一条 UPDATE 处理整个数组
UPDATE demo_order SET status = 'done' WHERE id = ANY(p_ids);= ANY(array) 是数组参数场景下最重要的优化手段。凡是「对数组里每个元素做同样的 DML」的写法,几乎都可以改写成 = ANY()。
实战示例
场景一:批量入库校验并写入
需求:接口一次提交多行库存变动,需要逐行校验 SKU 是否存在、数量是否合法,合法的写入明细表,非法的收集起来返回给调用方。
建表:
CREATE TABLE IF NOT EXISTS demo_stock(
sku text PRIMARY KEY,
qty int NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS demo_stock_move(
id bigserial PRIMARY KEY,
sku text NOT NULL,
qty int NOT NULL,
moved_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO demo_stock(sku, qty) VALUES ('SKU-1', 100), ('SKU-2', 50)
ON CONFLICT (sku) DO NOTHING;建函数:
CREATE OR REPLACE FUNCTION app_batch_move(p_moves demo_stock_move[])
RETURNS TABLE(ok_cnt int, bad_list text[])
LANGUAGE plpgsql AS $$
DECLARE
v_m demo_stock_move;
v_ok int := 0;
v_bad text[] := ARRAY[]::text[];
v_exists boolean;
BEGIN
FOREACH v_m IN ARRAY p_moves LOOP
-- 校验 1:数量必须非零
IF v_m.qty = 0 THEN
v_bad := v_bad || format('%s:数量为零', v_m.sku);
CONTINUE;
END IF;
-- 校验 2:SKU 必须存在
SELECT EXISTS(SELECT 1 FROM demo_stock WHERE sku = v_m.sku) INTO v_exists;
IF NOT v_exists THEN
v_bad := v_bad || format('%s:SKU 不存在', v_m.sku);
CONTINUE;
END IF;
-- 校验 3:扣减后不能为负
IF (SELECT qty FROM demo_stock WHERE sku = v_m.sku) + v_m.qty < 0 THEN
v_bad := v_bad || format('%s:库存不足', v_m.sku);
CONTINUE;
END IF;
-- 全部通过:写明细 + 更新库存
INSERT INTO demo_stock_move(sku, qty) VALUES (v_m.sku, v_m.qty);
UPDATE demo_stock SET qty = qty + v_m.qty WHERE sku = v_m.sku;
v_ok := v_ok + 1;
END LOOP;
ok_cnt := v_ok;
bad_list := v_bad;
RETURN NEXT;
END;
$$;调用与输出:
SELECT * FROM app_batch_move(ARRAY[
ROW('SKU-1', 10)::demo_stock_move,
ROW('SKU-9', 5)::demo_stock_move,
ROW('SKU-2',-80)::demo_stock_move,
ROW('SKU-2', 0)::demo_stock_move
]); ok_cnt | bad_list
--------+-------------------------------------------
1 | {"SKU-9:SKU 不存在","SKU-2:库存不足","SKU-2:数量为零"}
(1 row)验证库存变动:
SELECT sku, qty FROM demo_stock ORDER BY sku; sku | qty
-------+-----
SKU-1 | 110
SKU-2 | 50
(2 rows)只有第一行通过校验,库存加上了 10;其余三行都被收集进 bad_list。用数组累积错误信息(v_bad := v_bad || ...)是批量接口的常见做法。
场景二:二维数组遍历做价格矩阵调整
需求:给一张「区域 × 商品」的价格矩阵整体上浮一定比例,需要逐单元格计算并回写,同时统计调整的总金额变化。
建表与造数:
CREATE TABLE IF NOT EXISTS demo_price(
region text NOT NULL,
sku text NOT NULL,
price numeric(12,2) NOT NULL,
PRIMARY KEY(region, sku)
);
INSERT INTO demo_price(region, sku, price) VALUES
('east', 'SKU-1', 100.00),
('east', 'SKU-2', 200.00),
('west', 'SKU-1', 110.00),
('west', 'SKU-2', 190.00);建函数,用两层 FOREACH 分别遍历「地区数组」与「SKU 数组」,做笛卡尔组合:
CREATE OR REPLACE FUNCTION app_raise_price_matrix(p_ratio numeric)
RETURNS TABLE(updated int, delta numeric)
LANGUAGE plpgsql AS $$
DECLARE
v_regions text[] := ARRAY['east', 'west'];
v_skus text[] := ARRAY['SKU-1', 'SKU-2'];
v_region text;
v_sku text;
v_old numeric;
v_new numeric;
v_cnt int := 0;
v_delta numeric := 0;
BEGIN
if p_ratio <= 0 THEN
RAISE EXCEPTION '上浮比例必须为正:%', p_ratio USING ERRCODE = '22023';
END IF;
FOREACH v_region IN ARRAY v_regions LOOP
FOREACH v_sku IN ARRAY v_skus LOOP
SELECT price INTO v_old
FROM demo_price WHERE region = v_region AND sku = v_sku;
IF NOT FOUND THEN
RAISE NOTICE '缺少 % / % 的价格,跳过', v_region, v_sku;
CONTINUE;
END IF;
v_new := round(v_old * (1 + p_ratio), 2);
v_delta := v_delta + (v_new - v_old);
UPDATE demo_price SET price = v_new
WHERE region = v_region AND sku = v_sku;
v_cnt := v_cnt + 1;
END LOOP;
END LOOP;
updated := v_cnt;
delta := v_delta;
RETURN NEXT;
END;
$$;调用与输出:
SELECT * FROM app_raise_price_matrix(0.10); updated | delta
---------+--------
4 | 60.00
(1 row)SELECT region, sku, price FROM demo_price ORDER BY region, sku; region | sku | price
--------+-------+--------
east | SKU-1 | 110.00
east | SKU-2 | 220.00
west | SKU-1 | 121.00
west | SKU-2 | 209.00
(4 rows)这里用两层 FOREACH 遍历两个一维数组来构造「二维」遍历。之所以不用真正的二维数组,是因为业务数据来自表而非内存矩阵;当数据在表里时,FOR ... IN SELECT 反而是更自然的选择——本例用数组是为了演示二维嵌套的写法。
注意事项
1. NULL 数组和空数组都是零次循环,不报错
这是特性,但容易掩盖「参数没传」的问题。
-- 不会报错,也不会执行循环体
FOREACH v_id IN ARRAY p_ids LOOP ... END LOOP;
-- 需要区分时,循环前先判断
IF p_ids IS NULL THEN RETURN 0; END IF;2. array_length(空数组, 1) 返回 NULL 而非 0
用 cardinality() 更稳妥。
SELECT array_length(ARRAY[]::int[], 1); -- NULL
SELECT cardinality(ARRAY[]::int[]); -- 03. 目标变量类型必须与元素类型一致
类型不匹配会在运行时报错。
-- 错误!元素是 bigint,变量是 text
-- FOREACH v_name IN ARRAY p_ids LOOP4. 不要硬编码数组下标从 1 开始
数组下界可以自定义,FOREACH 不受影响,但手写下标会出问题。
-- 不推荐:假设下界是 1
-- FOR i IN 1..array_length(v_arr,1) LOOP ... v_arr[i] ... END LOOP;
-- 正确:用实际边界
FOR i IN array_lower(v_arr,1)..array_upper(v_arr,1) LOOP
RAISE NOTICE '%', v_arr[i];
END LOOP;5. SLICE 时目标变量必须是数组
加了 SLICE n 后,目标接收的是 n 维切片,所以必须是数组类型;数组维度要与 SLICE 的值对应,声明成标量会报类型不匹配。
-- 正确:SLICE 1 时目标是一维数组
FOREACH v_row_slice SLICE 1 IN ARRAY v_m LOOP -- 一行切片
-- 错误:目标声明成标量
-- FOREACH v_cell SLICE 1 IN ARRAY v_m LOOP6. 循环体里的 SQL 会被执行 N 次
数组元素多时,逐元素 DML 的性能会很差。
-- 不推荐:N 次 UPDATE
-- FOREACH v_id IN ARRAY p_ids LOOP
-- UPDATE demo_order SET status='done' WHERE id = v_id;
-- END LOOP;
-- 推荐:一条语句
UPDATE demo_order SET status = 'done' WHERE id = ANY(p_ids);7. 用 FOREACH 无法获知元素下标
需要位置信息时必须自己计数,或改用 unnest(...) WITH ORDINALITY。
-- 需要下标时的替代方案
FOR v_r IN SELECT value, ordinality FROM unnest(p_ids) WITH ORDINALITY AS t(value, ordinality) LOOP
RAISE NOTICE '第 % 个:%', v_r.ordinality, v_r.value;
END LOOP;8. 数组拼接是 O(n),不要在大循环里反复 ||
v_bad := v_bad || x 每次都复制整个数组。数据量大时累加成本很高。
-- 数据量大时改用临时表或直接在循环里 INSERT
-- CREATE TEMP TABLE tmp_bad(msg text); 循环里 INSERT INTO tmp_bad VALUES (...);9. 别把数组当表用
「先在函数里把大批数据塞进数组,再遍历处理」往往不如直接写一条 SQL。数组适合传递参数,不适合当数据缓存。
-- 不推荐:先把全表读进数组
-- SELECT array_agg(id) INTO v_ids FROM demo_order;
-- FOREACH v_id IN ARRAY v_ids LOOP ... END LOOP;
-- 推荐:直接遍历查询
FOR v_r IN SELECT id FROM demo_order LOOP ... END LOOP;10. 多维数组要求各维长度一致
ARRAY[[1,2],[3]] 是非法的。构造矩阵数据时要保证规整。
-- 错误!各维长度不一致
-- SELECT ARRAY[[1,2],[3]];
-- 报错:multidimensional arrays must have array expressions with matching dimensions
-- 正确
SELECT ARRAY[[1,2],[3,4]];最佳实践
1. 纯数组逐项处理优先用 FOREACH
比手写下标遍历简洁、安全,也不受数组下界影响。
FOREACH v_id IN ARRAY p_ids LOOP
PERFORM app_touch(v_id);
END LOOP;2. 需要下标时用 unnest(...) WITH ORDINALITY
这是获得位置信息的标准做法。
FOR v_r IN
SELECT value, ordinality FROM unnest(p_ids) WITH ORDINALITY AS t(value, ordinality)
LOOP
RAISE NOTICE '第 % 位 = %', v_r.ordinality, v_r.value;
END LOOP;3. 进入循环前先校验数组参数
把 NULL、空数组、非法元素提前挡掉,主循环逻辑更干净。
IF p_ids IS NULL OR cardinality(p_ids) = 0 THEN
RAISE NOTICE '没有需要处理的 id';
RETURN 0;
END IF;4. 用 cardinality() 而不是 array_length() 统计元素数
空数组场景下行为更符合直觉。
RAISE NOTICE '共 % 个元素', cardinality(p_ids);5. 对数组做同一操作时用 = ANY(...) 而不是循环
这是数组参数最重要的性能优化。
-- 推荐
UPDATE demo_order SET status = 'done' WHERE id = ANY(p_ids);
-- 而非循环逐条 UPDATE6. 循环体保持短小,复杂逻辑封装成函数
循环只负责「迭代」,业务逻辑放在独立函数里,便于测试与复用。
FOREACH v_m IN ARRAY p_moves LOOP
PERFORM app_apply_one_move(v_m.sku, v_m.qty);
END LOOP;7. 用 CONTINUE WHEN 提前跳过
减少嵌套层级,把「不处理」的情况写在最前面。
FOREACH v_m IN ARRAY p_moves LOOP
CONTINUE WHEN v_m.qty = 0;
...
END LOOP;8. 收集错误信息用数组或临时表,别用字符串拼接
数组更结构化,临时表在数据量大时更高效。
v_bad := v_bad || format('%s:SKU 不存在', v_m.sku);9. 嵌套遍历时给外层加标签
后续要跨层跳出时不用改结构。
<<outer_arr>>
FOREACH v_a IN ARRAY p_rows LOOP
FOREACH v_b IN ARRAY p_cols LOOP
IF cond THEN EXIT outer_arr; END IF;
END LOOP;
END LOOP outer_arr;10. 用 SLICE 处理矩阵,避免手动算下标
矩阵数据用 SLICE 1 取行、SLICE 2 取单元格,比手算 i*cols+j 清晰得多。
FOREACH v_row SLICE 1 IN ARRAY v_matrix LOOP
PERFORM app_handle_row(v_row);
END LOOP;11. 记录处理计数,便于对账
批量接口返回「成功几条 / 失败几条」是基本要求,循环里顺手统计。
v_ok := v_ok + 1;
...
ok_cnt := v_ok;12. 大批量输入考虑改为 unnest + 一条 SQL
如果整个批量操作能表达成一条 SQL,就不要用 FOREACH 逐条执行。
-- 推荐:一条语句完成批量插入
INSERT INTO demo_stock_move(sku, qty)
SELECT sku, qty FROM unnest(p_skus, p_qtys) AS t(sku, qty);下一节