Skip to content

遍历数组(FOREACH) ​

上一节用 FOR ... IN SELECT 遍历查询结果,这一节换一种数据源:数组。PL/pgSQL 提供了 FOREACH 语句,专门用来按顺序遍历数组元素。它在「批量参数逐项处理」「二维矩阵遍历」「JSON 数组展开」等场景下比展开成查询再遍历更直接。

本节讲解 FOREACH 的语法、一维与多维(SLICE)遍历、NULL 数组与空数组的处理,以及它与 unnest() 方案的取舍。

前置知识

阅读本节前,建议先了解:遍历查询结果(FOR ... IN SELECT)

基础概念 ​

为什么需要 FOREACH ​

数组在 PostgreSQL 里是一等公民:函数参数可以收数组,array_agg() 可以产出数组,text[] 是批处理接口的常见入参类型。拿到一个数组后,最常见的需求就是「逐个元素处理」。

用整数 FOR 也可以做到,但要手动算下标:

sql
-- 手写下标遍历:啰嗦且容易越界
FOR v_i IN 1..array_length(v_ids, 1) LOOP
  v_id := v_ids[v_i];
  ...
END LOOP;

FOREACH 把这件事简化为一行:

sql
-- 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 按数组的实际下标顺序遍历,与下界是多少无关。

text
数组 v_arr := ARRAY['a','b','c']
下标:      1    2    3
FOREACH 顺序: 'a' → 'b' → 'c'

NULL 与空数组 ​

两个容易混淆的情况:

  • NULL 数组:根本不进入循环体,一次都不执行。
  • 空数组:同样不进入循环体,一次都不执行。

两者在 FOREACH 下表现一致(都是零次),但含义不同——如果需要区分「没传」和「传了空」,要在循环外先判断 IS NULL。

语法与用法 ​

完整语法:

text
[ <<label>> ]
FOREACH 目标 [ SLICE 数字 ] IN ARRAY 数组表达式 LOOP
    语句;
END LOOP [ label ];

语法要点:

text
FOREACH 目标 [ SLICE n ] IN ARRAY 数组表达式 LOOP
        │         │             │
        │         │             └── 任意求值为数组的表达式
        │         │                  (变量、函数调用、ARRAY[...] 字面量)
        │         │
        │         └── 可选。n = 1 时目标接收「子数组」而非单个元素
        │             用于一次取出一维切片
        │
        └── 单个变量。类型与数组元素类型一致
            (多维遍历时可以是数组类型)

END LOOP [ label ];

最小示例:

sql
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;
$$;
text
NOTICE:  遍历到:alice
NOTICE:  遍历到:bob
NOTICE:  遍历到:carol

元素类型可以是任意类型,包括复合类型与 record:

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

详细说明 ​

一维数组遍历:最基本形态 ​

一维数组是最常见的用法。目标变量的类型要和数组元素类型一致,否则会在运行时报类型不匹配错误。

sql
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 + 下标:需要真正用到原始下标时用。
sql
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 数组和空数组都是「零次循环」。这一点与很多人的直觉不同——它不会抛异常。

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

如果需要区分两种情况,要在进入循环前判断:

sql
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,这是一个常见陷阱:

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

sql
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;
$$;
text
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 会逐元素遍历,多维数组被按存储顺序摊平成标量序列:

sql
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 遍历,也是一种常见做法:

sql
-- 方案 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;

两种方案的对比:

维度FOREACHunnest() + FOR ... IN SELECT
语法简洁度更简洁略啰嗦
能否拿到下标不能(需自维护)能(WITH ORDINALITY)
能否顺便 JOIN不能能(unnest 可与其他表连接)
结果集是否物化数组已在内存中会多一层结果集
适用场景纯数组逐项处理需要下标或需要与表连接

选择原则:只要不需要下标、不需要 JOIN,就用 FOREACH。

sql
-- 推荐:纯遍历
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)。

sql
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 从查询结果得到:

sql
SELECT app_apply_moves(ARRAY[
  ROW('SKU-1', 10)::demo_stock_move,
  ROW('SKU-2', -3)::demo_stock_move
]);

循环控制:EXIT 与 CONTINUE ​

FOREACH 支持全部循环控制语句,用法与其它循环一致,包括标签跳转。

sql
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

嵌套数组遍历时,标签让跨层跳出变得清晰:

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

sql
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 条语句。

sql
-- 不推荐: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 是否存在、数量是否合法,合法的写入明细表,非法的收集起来返回给调用方。

建表:

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

建函数:

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

调用与输出:

sql
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
]);
text
 ok_cnt |                bad_list
--------+-------------------------------------------
      1 | {"SKU-9:SKU 不存在","SKU-2:库存不足","SKU-2:数量为零"}
(1 row)

验证库存变动:

sql
SELECT sku, qty FROM demo_stock ORDER BY sku;
text
  sku  | qty
-------+-----
 SKU-1 | 110
 SKU-2 |  50
(2 rows)

只有第一行通过校验,库存加上了 10;其余三行都被收集进 bad_list。用数组累积错误信息(v_bad := v_bad || ...)是批量接口的常见做法。

场景二:二维数组遍历做价格矩阵调整 ​

需求:给一张「区域 × 商品」的价格矩阵整体上浮一定比例,需要逐单元格计算并回写,同时统计调整的总金额变化。

建表与造数:

sql
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 数组」,做笛卡尔组合:

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

调用与输出:

sql
SELECT * FROM app_raise_price_matrix(0.10);
text
 updated | delta
---------+--------
       4 |  60.00
(1 row)
sql
SELECT region, sku, price FROM demo_price ORDER BY region, sku;
text
 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 数组和空数组都是零次循环,不报错

这是特性,但容易掩盖「参数没传」的问题。

sql
-- 不会报错,也不会执行循环体
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() 更稳妥。

sql
SELECT array_length(ARRAY[]::int[], 1);  -- NULL
SELECT cardinality(ARRAY[]::int[]);      -- 0

3. 目标变量类型必须与元素类型一致

类型不匹配会在运行时报错。

sql
-- 错误!元素是 bigint,变量是 text
-- FOREACH v_name IN ARRAY p_ids LOOP

4. 不要硬编码数组下标从 1 开始

数组下界可以自定义,FOREACH 不受影响,但手写下标会出问题。

sql
-- 不推荐:假设下界是 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 的值对应,声明成标量会报类型不匹配。

sql
-- 正确:SLICE 1 时目标是一维数组
FOREACH v_row_slice SLICE 1 IN ARRAY v_m LOOP   -- 一行切片
-- 错误:目标声明成标量
-- FOREACH v_cell SLICE 1 IN ARRAY v_m LOOP

6. 循环体里的 SQL 会被执行 N 次

数组元素多时,逐元素 DML 的性能会很差。

sql
-- 不推荐: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。

sql
-- 需要下标时的替代方案
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 每次都复制整个数组。数据量大时累加成本很高。

sql
-- 数据量大时改用临时表或直接在循环里 INSERT
-- CREATE TEMP TABLE tmp_bad(msg text);  循环里 INSERT INTO tmp_bad VALUES (...);

9. 别把数组当表用

「先在函数里把大批数据塞进数组,再遍历处理」往往不如直接写一条 SQL。数组适合传递参数,不适合当数据缓存。

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]] 是非法的。构造矩阵数据时要保证规整。

sql
-- 错误!各维长度不一致
-- SELECT ARRAY[[1,2],[3]];
-- 报错:multidimensional arrays must have array expressions with matching dimensions

-- 正确
SELECT ARRAY[[1,2],[3,4]];

最佳实践 ​

1. 纯数组逐项处理优先用 FOREACH

比手写下标遍历简洁、安全,也不受数组下界影响。

sql
FOREACH v_id IN ARRAY p_ids LOOP
  PERFORM app_touch(v_id);
END LOOP;

2. 需要下标时用 unnest(...) WITH ORDINALITY

这是获得位置信息的标准做法。

sql
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、空数组、非法元素提前挡掉,主循环逻辑更干净。

sql
IF p_ids IS NULL OR cardinality(p_ids) = 0 THEN
  RAISE NOTICE '没有需要处理的 id';
  RETURN 0;
END IF;

4. 用 cardinality() 而不是 array_length() 统计元素数

空数组场景下行为更符合直觉。

sql
RAISE NOTICE '共 % 个元素', cardinality(p_ids);

5. 对数组做同一操作时用 = ANY(...) 而不是循环

这是数组参数最重要的性能优化。

sql
-- 推荐
UPDATE demo_order SET status = 'done' WHERE id = ANY(p_ids);
-- 而非循环逐条 UPDATE

6. 循环体保持短小,复杂逻辑封装成函数

循环只负责「迭代」,业务逻辑放在独立函数里,便于测试与复用。

sql
FOREACH v_m IN ARRAY p_moves LOOP
  PERFORM app_apply_one_move(v_m.sku, v_m.qty);
END LOOP;

7. 用 CONTINUE WHEN 提前跳过

减少嵌套层级,把「不处理」的情况写在最前面。

sql
FOREACH v_m IN ARRAY p_moves LOOP
  CONTINUE WHEN v_m.qty = 0;
  ...
END LOOP;

8. 收集错误信息用数组或临时表,别用字符串拼接

数组更结构化,临时表在数据量大时更高效。

sql
v_bad := v_bad || format('%s:SKU 不存在', v_m.sku);

9. 嵌套遍历时给外层加标签

后续要跨层跳出时不用改结构。

sql
<<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 清晰得多。

sql
FOREACH v_row SLICE 1 IN ARRAY v_matrix LOOP
  PERFORM app_handle_row(v_row);
END LOOP;

11. 记录处理计数,便于对账

批量接口返回「成功几条 / 失败几条」是基本要求,循环里顺手统计。

sql
v_ok := v_ok + 1;
...
ok_cnt := v_ok;

12. 大批量输入考虑改为 unnest + 一条 SQL

如果整个批量操作能表达成一条 SQL,就不要用 FOREACH 逐条执行。

sql
-- 推荐:一条语句完成批量插入
INSERT INTO demo_stock_move(sku, qty)
SELECT sku, qty FROM unnest(p_skus, p_qtys) AS t(sku, qty);

下一节

继续学习:退出与跳过(EXIT / CONTINUE)

参考链接 ​